说到排序算法,伙伴们必定很熟悉,包含在做算法题的时候,许多时候是需求将数组排序,然后求得终究的结果,那么此刻咱们就会考虑算法完结的时刻复杂度以及空间复杂度,接下来我会介绍常见的排序算法。

1 冒泡排序

思维

遍历数组中的元素,每轮拿到数组中最大的值,放在数组后面的方位,直到排序完结。

完结

/**
 * 冒泡排序
 *
 * @param arr 无序数组
 * @return 有序数组
 */
public static int[] BubbleSort(int[] arr) {
    if (arr == null || arr.length < 1) {
        return arr;
    }
    for (int i = 0; i < arr.length; i  ) {
        //外层控制遍历的次数
        for (int j = 0; j < arr.length - i - 1; j  ) {
            if (arr[j] > arr[j   1]) {
                swap(arr,j,j 1);
            }
        }
    }
    return arr;
}

经过两个for循环,其间外层的循环用于控制遍历的轮数,内层的循环用于比较巨细,然后交流方位,总是把大的数字往后放,当一轮遍历完结之后,数组中最大的数字放在末尾,此刻内层的循环将缩小查找规模(由外层循环决议),直到遍历完结。

时刻复杂度和空间复杂度

时刻复杂度O(n^2)

空间复杂度O(1)

2 挑选排序

思维

每次遍历悉数的元素,找到数组中最小值的索引,将其放在数组前面的方位,直到遍历完结悉数的元素。

完结

public static int[] SelectSort(int[] arr){
    for (int i = 0;i<arr.length;i  ){
        int index = i;
        for (int j = i;j<arr.length;j  ){
            if(arr[j] < arr[index]){
                index = j;
            }
        }
        //交流方位
        if (index !=i){
            swap(arr,i,index);
        }
    }
    return arr;
}

经过两个for循环,外层循环控制索引的方位,内层循环用于比较巨细,每次默认最小值是外层循环的索引方位,内部循环判别假设有最小的值,那么就更新index的值;

当遍历完结之后,交流方位,有一个小优化,假设后面没有比当时元素更小的值,可以不交流方位,持续下一次遍历。

时刻复杂度和空间复杂度

时刻复杂度O(n^2)

空间复杂度O(1)

3 插入排序

思维

遍历整个数组,每个元素需求判别与前面元素的巨细,假设比前面的元素小,就交流方位。

完结

public static int[] InsertSort(int[] arr){
    for (int i = 0;i<arr.length;i  ){
        int j = i;
        while (j > 0){
            if (arr[j] < arr[j-1]){
                swap(arr,j,j-1);
            }
            j--;
        }
    }
    return arr;
}

时刻复杂度和空间复杂度

时刻复杂度O(n^2)

空间复杂度O(1)

4 归并排序

思维

归并排序的思维其实便是两个有序的数组,经过两个指针指向两个数组的头部,顺次比较数组中每一个元素,终究合并成一个有序数组。归并的思维在许多算法题中也有表现,链表、数组中涉及到排序的问题。

完结

首要关于数组中每个独自的元素来说,便是有序的,在遍历的进程中,拆分最小颗粒度,由小的有序数组合并成大的有序数组。

public static int[] MergeSort(int[] arr) {
    process(arr, 0, arr.length - 1);
    return arr;
}
private static void process(int[] arr, int left, int right) {
    if (left == right) {
        return;
    }
    //将数组分红两份
    int middle = (right   left) / 2;
    process(arr, left, middle);
    process(arr, middle 1, right);
    merge(arr,left,middle,right);
}
private static void merge(int[] arr,int left,int middle,int right) {
    int[] help = new int[right - left   1];
    int p1 = left;
    int p2 = middle 1;
    int h1 = 0;
    while (p1 <= middle && p2 <= right) {
        if (arr[p1] < arr[p2]) {
            help[h1] = arr[p1];
            p1  ;
        } else {
            help[h1] = arr[p2];
            p2  ;
        }
        h1  ;
    }
    if (p1 <= middle) {
        for (int i = p1; i <= middle; i  ) {
            help[h1] = arr[p1];
            h1  ;
        }
    }
    if (p2 <= right) {
        for (int i = p2; i <= right; i  ) {
            help[h1] = arr[p2];
            h1  ;
        }
    }
    for (int i = 0;i<help.length;i  ){
        arr[left i] = help[i];
    }
}

5 快速排序

思维

快速排序其实是“国旗”问题的延伸,给定一个基准值,让一个数组根据此基准值区分,左面均小于基准值,右边均大于基准值,然后经过递归完结整个数组的排序。

荷兰国旗问题

给定一个数组和一个基准值,让小于基准值的数字在左面,大于基准值的数字在右边,不要求左右两边是排序的。

数据结构与算法 -- LeetCode中常见的排序算法题

思路

界说一个鸿沟区间,界说一个指针指向数组的第一个方位,主要分为以下几个case

  • case1:当时数值假设小于基准值,那么就与鸿沟的下一个方位的元素互换方位,一起鸿沟值 ,指针 ;
  • case2:当时数值假设大于基准值,那么指针 。

完结

public static int[] Solution(int[] arr, int num) {
    int i = 0;
    //鸿沟
    int less = -1;
    while (i < arr.length) {
        if (arr[i] <= num) {
            //当时值与鸿沟的下一个元素交流
            swap(arr, i,   less);
        }
        i  ;
    }
    return arr;
}

前面咱们说到了,在分组的进程中,咱们不要求有序,只要小于基准值的就放在左面,大于基准值的放在右边,此刻这个问题的升级版便是,闻名的荷兰国旗问题,将数组分为3份,左面小于基准值,中心等于基准值,右边大于基准值。

数据结构与算法 -- LeetCode中常见的排序算法题

此刻界说两个鸿沟,左面界是小于基准值的区域,右鸿沟是大于基准值的区域,一起两边推,终究中心的区域便是等于基准值的区域,因而会有以下case:

  • case1:假设当时值小于基准值,那么当时值与左面界的下个元素做交流;左面界 ,i ;
  • case2:假设当时值等于基准值,i ;
  • case3:假设当时值大于基准值,那么当时值与有鸿沟的前一个元素做交流;有鸿沟–,i不动,为啥不动,因为换过来的值可能射中其间某个case,还需求判别。
public static int[] Solution2(int[] arr, int num) {
    int i = 0;
    //左面界
    int less = -1;
    //有鸿沟
    int more = arr.length;
    while (i < more) {
        if (arr[i] <= num) {
            //当时值与鸿沟的下一个元素交流
            swap(arr, i,   less);
            i  ;
        } else if (arr[i] == num) {
            i  ;
        } else {
            //当时值大于基准值
            swap(arr, --more, i);
        }
    }
    return arr;
}

快速排序1.0

当你理解了”荷兰国旗”的原理之后,就会很快理解快速排序的真理。其实快速排序1.0的原理便是如下图所示:

数据结构与算法 -- LeetCode中常见的排序算法题

一次partition的进程便是,取数组最终一个元素作为基准值,将数组分为大于基准值和小于基准值的部分,然后将基准值与大于基准值部分的第一个元素交流。此刻基准值一定是在有序数组中的方位

随后在大于基准值和小于基准值的部分别离进行partition的操作,终究完结排序。

这个思维与荷兰国旗的初始版别共同,基准值需求做一次替换,然后定位到有序数组的方位。

快速排序2.0

快速排序2.0则是根据荷兰国旗的升级版,在partition的进程中,就完结分组(>num 、=num、<num),此刻基准值就现已找到了在有序数组的方位,然后在 > num 和 < num的分组中别离进行partition。

public static int[] QuickSort_2(int[] arr) {
    return QuickSortInner(arr, 0, arr.length - 1);
}
private static int[] QuickSortInner(int[] arr, int left, int right) {
    if (left < right) {
        int pos = partition(arr, left, right);
        QuickSortInner(arr, left, pos - 1);
        QuickSortInner(arr, pos   1, right);
    }
    return arr;
}
/**
 * 回来基准值的方位
 *
 * @param arr   整个数组
 * @param left  需求排序的左面方位
 * @param right 需求排序的右边方位
 */
public static int partition(int[] arr, int left, int right) {
    //基准值
    int num = arr[right];
    int i = left;
    //左面界
    int less = left - 1;
    //有鸿沟
    int more = right   1;
    while (i < more) {
        if (arr[i] < num) {
            swap(arr,   less, i);
            i  ;
        } else if (arr[i] == num) {
            i  ;
        } else {
            swap(arr, --more, i);
        }
    }
    return less   1;
}

时刻复杂度和空间复杂度

快速排序其实仍是有3.0版别的,不再以数组的最终一个数字为基准值,而是选用随机数的方法,可是快速排序最差的状况,时刻复杂度为O(n^2),均匀下来为O(nlogn),空间复杂度为O(logn)。

6 堆排序

堆排序,其实便是用数组完结彻底二叉树,什么是彻底二叉树?

数据结构与算法 -- LeetCode中常见的排序算法题

看上图,假设一棵二叉树的所有节点均存在左右节点,或许从左往右有排满的趋势,那么就认为是彻底二叉树,例如倒数第二棵二叉树,虽然第三层没有满,可是从左往右看是接连的,这便是有排满的趋势,所以是彻底二叉树;可是倒数第一棵二叉树,第三层不是接连的,因而不是彻底二叉树。

而堆则是一种比较特别的彻底二叉树,分为大根堆和小根堆,如下图所示。

数据结构与算法 -- LeetCode中常见的排序算法题

大根堆指的是恣意一棵子树,头结点都是这棵树的最大值;小根堆指的是恣意一棵子树,头结点都是这棵树的最小值。

思路

前面咱们主要是讲解了彻底二叉树的一些基础知识,因为数组是接连的,所以构建的彻底二叉树天然也得是接连的,所以关于如何经过索引找到其左节点、右节点、父节点,记住公式:

数据结构与算法 -- LeetCode中常见的排序算法题

所以假设要构建一个大根堆,那么在遍历到某个元素时,与自己的父节点比较,假设比自己的父节点小,那么就持续下个元素的比较;假设比自己的父节点值要大,那么就交流,交流完结之后,假设还有父节点就需求一向比较,一向往上窜。

完结大根堆

首要第一步,遍历数组中的元素,构建彻底二叉树,可是要符合大根堆的条件:

数据结构与算法 -- LeetCode中常见的排序算法题

当顺次遍历数组中的元素时,开端构建彻底二叉树(脑海中),当遍历到数字8时,此刻二叉树不符合大根堆的条件,此刻需求交流与父节点的方位,此刻index = 3,其父节点的方位为(index-1)/2,交流完结之后,发现仍是不满足大根堆的条件,因而持续往上找,直到符合条件。

交流完结之后,持续遍历,遇到不满足大根堆的条件就进行转换,直到遍历完结,这个进程叫做HeapInsert.

/**
 * Heap Insert操作
 *
 * @param arr   待排序的数组
 * @param index 当时遍历的元素
 */
private static void heapInsert(int[] arr, int index) {
    while (arr[index] > arr[(index - 1) / 2]) {
        //当时元素,比父节点要大
        swap(arr, index, (index - 1) / 2);
        index = (index-1) / 2;
    }
}

此刻数组中的第一个节点为最大值,假设要获取这个数组中的最大值,那么直接回来数组中第0个元素即可。

假设删去最大值,还要保证是一个最大堆的条件,那么首要把数组中最终一个元素替换到首位。

数据结构与算法 -- LeetCode中常见的排序算法题

在替换完结之后,可能会呈现不满足大根堆的条件,此刻需求从头结点开端,比较左右孩子,拿到最大的值,与头结点替换;替换完结后,此刻头结点到了子节点的方位,还需求持续判别它的左右孩子是否满足大根堆的条件,这是heapify的进程。

/**
 * Heapif操作
 *
 * @param arr   待排序的数组
 * @param index 头结点的索引
 */
private static void heapify(int[] arr, int heap_size, int index) {
    int left = 2 * index   1;
    while (left < heap_size) {
        //根节点
        int head = arr[index];
        //两个孩子的最大节点
        int largestChild = left;
        //先比较两个孩子
        //假设有右孩子,而且右孩子比左孩子大,那么两个孩子的最大节点便是右孩子
        //不然便是左孩子
        if (left   1 < heap_size && arr[left   1] > arr[left]) {
            largestChild = left   1;
        }
        if (head < arr[largestChild]) {
            //交流
            swap(arr, index, largestChild);
        }
        index = largestChild;
        left = 2 * index   1;
    }
}

如此操作之后,所有最大堆的根节点都被放在的数组的后面,而且仍是排序的,终究拿到的数组便是排序完结的。

public static int[] HeapSort(int[] arr) {
    int heap_size = 0;
    while (heap_size < arr.length) {
        heapInsert(arr, heap_size);
        heap_size  ;
    }
    while (heap_size > 0) {
        heap_size--;
        swap(arr, heap_size, 0);
        heapify(arr, heap_size, 0);
    }
    return arr;
}

时刻复杂度和空间复杂度

时刻复杂度O(nlogn)

空间复杂度O(1)

7 计数排序

前面咱们见到的排序算法,均为数字之间的比较,经过比较巨细交流次序然后完结数组的排序,而计数排序和桶排序,则是在空间上的计数,然后完结排序规矩的复原。

思路

举个例子,有一组职工的年纪数组,咱们知道职工的年纪是在[16-100]这个区间内的,超过或许低于这个区间咱们认为是不合理的,因而咱们可以创立一个数组长度为100的空数组,遍历一次数组,那么在空数组中便可以得到每个数字的计数。

例如:16呈现2次,20呈现3次,21呈现1次,35呈现1次,40呈现2次,那么终究的排序便是[16,16,20,20,20,35,40,40].

所以,此类排序使用的场景有限:假设数组中的元素便是随机数,在-10^5<= num <= 10^5 之间,显然就不能使用这种计数的方法了。

完结

public static int[] CountSort(int[] arr) {
    int max = getMaxValue(arr);
    int[] res = new int[max   1];
    for (int i = 0; i < arr.length; i  ) {
        res[arr[i]]  ;
    }
    int index = 0;
    for (int i = 0; i < res.length; i  ) {
        while (res[i] > 0) {
            arr[index] = i;
            index  ;
            res[i]--;
        }
    }
    return arr;
}
private static int getMaxValue(int[] arr) {
    int max = Integer.MIN_VALUE;
    for (int i = 0; i < arr.length; i  ) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    return max;
}

8 基数排序 & 桶排序

桶排序或许基数排序,是根据计数排序的升级版别,因为计数排序存在的弊端在于,必需要直到数组中的最大值,决议映射数组的长度。

未完待续~

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。