排序算法总结

0. 排序算法的分类

  1. 根据待排序的数据大小不同,使得排序过程中所涉及的存储器不同,可分为: 内部排序(内存足够) 、外部排序 (还需访问外存)

  2. 排序关键字可能出现重复,根据重复关键字的排序情况可分为: 稳定排序(排序后重复关键字记录的相对次序保持不变) 、不稳定排序。一般来说,归并排序 和 快速排序 是不稳定的排序,但是也能够使用特殊方法实现稳定的排序,只不过实现起来比较难。

  3. 对于内部排序,依据不同的排序原则,可分为: 插入排序、交换排序 、选择排序 、归并排序 、计数排序
    在这里插入图片描述

  4. 针对内部排序所需的工作量划分,可分为: 简单排序 O(n^2) 、先进排序 O(nlogn) 、基数排序 O(d*n)

  5. 在这里插入图片描述

1. 插入排序

1.1 直接插入排序

插入排序的基本思想是: 在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。

public class InsertionSort {

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

    public static void swap(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    public static void main(String[] args){
        ...
    }
    
}

1.2 希尔排序

希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  1. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  2. 增量序列个数k,对序列进行k 趟排序;
  3. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
    在这里插入图片描述
public class ShellSort {
    public static void shellSort(int[] arr){
        int N = arr.length;
        //进行分组,最开始时的增量gap为数组长度的一半
        for(int gap = N/2; gap > 0; gap /= 2){
            //对每个分组进行插入排序
            for(int i = gap; i < N; i++){
                //将arr[i]插入到所在分组的正确位置上
                insertI(arr, gap, i);
            }
        }
    }
    
    /**
     *将arr[i]插入到所在分组的正确位置上
     * arr[i]所在的分组为:
     * ... arr[i-2*gap], arr[i-gap], arr[i], arr[i+gap],arr[i+2*gap]...
     *arr[i]之前的序列是有序的,依次和前面相隔gap的比较。随着for循环i的增加,整个数组有序。
     *
     *直接插入排序的代码:
     *    for (int j = i; j > 0 && arr[j] < arr[j - 1]; j--) {
     *        swap(arr, j - 1, j);
     *    }
     */
    public static void insertI(int[] arr, int gap, int i){
        for(int j = i; j > 0 && arr[j] < arr[j - gap]; j -= gap){
            swap(arr, j, j - gap);
        }
    }

    public static void swap(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a]= arr[b];
        arr[b]= temp;
    }

    public static void main(String[] args){
        int[] arr = new int[]{1,2,5,3,9,8,7,4,6};
        int len = arr.length;
        shellSort2(arr);
        for(int value : arr){
            System.out.print(value + " ");
        }
    }
}

和直接插入排序相比,多了一个控制gap的for循环

for(int gap = N/2; gap > 0; gap /= 2){
	...
}

2. 交换排序

2.1 冒泡排序

冒泡排序 是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

public class BubbleSort {

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

    public static void swap(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    
    public static void main(String[] args){
        ...
    }
    
}

2.2 快速排序

2.2.1 荷兰国旗问题

给定一个整数数组,给定一个值K,这个值在原数组中一定存在,要求把数组中小于K的元素放到数组的左边,大于K的元素放到数组的右边,等于K的元素放到数组的中间,最终返回一个整数数组。

要求:额外空间复杂度为O(1),时间复杂度为O(N)。

需要注意到的问题是:这个数组不需要有序。

public class NetherlandsFlag {

    public static int[] partition(int[] arr, int l, int r, int num){
        //将数组划分成三个区域,定义小于区域的右边界为less,大于区域的左边界为more
        int less = l - 1;
        int more = r + 1;
        //定义遍历指针current指向数组的第一个元素。
        int cur = l;

        while(cur < more){
            if(arr[cur] < num){
                //当前指针所指的元素值小于num,小于区域右边界向右移动一位,交换两值,交换之后,当前指针右移。
                swap(arr, ++less, cur++);
            }else if(arr[cur] > num){
                //当前指针所指元素大于num,大于区域左边界向左移动一位,交换两值,交换之后,当前指针不移动。
                swap(arr, --more, cur);
            }else{
                //如果当前指针所指元素等于num,当前指针右移。
                cur++;
            }
        }

        return arr;
    }
    
    public static void swap(int[] arr, int a, int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

    public static void main(String[] args){
        int[] arr = {0,5,1,2,9,8,7,4,5,6,2,5,8,4,3,7,1};
        partition(arr, 0, arr.length - 1, 5);
        for (int value : arr) {
            System.out.print(" " + value);
        }
        System.out.println("");
    }
}
2.2.2 经典快排

根据荷兰国旗问题,可以引申到经典快排

public class QuickSort2 {
    public static void quickSort(int[] arr, int l, int r){
        if(l < r){
            int[] p = partition(arr, l, r);
            quickSort(arr, l, p[0] - 1);
            quickSort(arr, p[1] + 1, r);
        }
    }


    public static int[] partition(int[] arr, int l, int r){
        /**
         * 与荷兰国旗问题的区别在于
         * 经典快排:根据数组最后一位元素值(num==arr[r])进行划分。
         * 改进版快排:根据数组中随机的一位元素值进行划分。
         */
        //将数组划分成三个区域,定义小于区域的右边界为less-1
        int less = l - 1;
        /**
         * 定义大于区域的左边界为more,每次划分以arr[r]为基准,且arr[r]不参与划分,
         * 当该划分完成之后,将数组最后一个元素arr[r]与大于区域的第一个元素交换,即swap(arr,more,r);
         * 至此三个区域划分完毕,一轮划分完成。
         */
        int more = r;
        //定义遍历指针current指向数组的第一个元素。
        int cur = l;

        while(cur < more){
            if(arr[cur] < arr[r]){
                //当前指针所指的元素值小于num,小于区域右边界向右移动一位,交换两值,交换之后,当前指针右移。
                swap(arr, ++less, cur++);
            }else if(arr[cur] > arr[r]){
                //当前指针所指元素大于num,大于区域左边界向左移动一位,交换两值,交换之后,当前指针不移动。
                swap(arr, --more, cur);
            }else{
                //如果当前指针所指元素等于num,当前指针右移。
                cur++;
            }
        }
        swap(arr,more,r);

        //返回一个长度为2的数组,两个元素的值分别为,中间元素相等区域的左右界限
        return new int[] {less + 1, more};
    }


    public static void swap(int[] arr, int a, int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

    public static void main(String[] args){
        int[] arr = {0,5,1,2,9,8,7,4,5,6,2,5,8,4,3,7,1};
        quickSort(arr, 0, arr.length - 1);
        for(int value : arr){
            System.out.print(" " + value);
        }
    }
}
2.2.3 随机快排

经典快排中,每次都选取数组中最后一个数为基准进行划分,这样有一个弊端。

如果数组已经有序,每次都选取最后一个数进行划分,(如果数组有序,那么这个数为这个数组的最值),会导致划分的结果只存在两种区域,小于或大于区域有可能没有。在这种情况下,执行N次划分,时间复杂度就会变成O(N2)。所以说,经典快排的时间复杂度与数组的数据状况有关。

我们可以把它改进一下,每次随机选取一个值,这样的话,无论选取值的好坏,即使它它是数组中的最值,也只是一个概率问题,最终的时间复杂度为O(N*logN),随机的额外空间复杂度为O(logN)

改进后的QuickSort代码如下

public class QuickSort3 {
    public static void quickSort(int[] arr, int l, int r){
        if(l < r){
            //改进后的快排
            swap(arr, l + (int)(Math.random() * (r - l + 1)), r);
            int[] p = partition(arr, l, r);
            quickSort(arr, l, p[0] - 1);
            quickSort(arr, p[1] + 1, r);
        }
    }
    ...
}

随机快排是最重要的排序,极为常用

3. 选择排序

3.1 简单选择排序

选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

步骤:

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。

  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

  3. 重复第二步,直到所有元素均排序完毕。

public class SelectionSort {

    public static int[] selectionSort(int[] arr) {

        for (int i = 0; i < arr.length - 1; i++) {
            int minIndex = i;
            for (int j = i + 1; j < arr.length; j++) {
                minIndex = arr[j] < arr[minIndex] ? j : minIndex;
            }
            swap(arr, i, minIndex);
        }
        return arr;
    }
    public static void swap(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    
    public static void main(String[] args){
        ...
    }
    
}

3.2 堆排序

满二叉树:所有父节点的左右孩子齐全

完全二叉树:在满二叉树的基础上,从左到右补叶节点的二叉树为完全二叉树

完全二叉树可以用数组表示:根节点从0开始,在数组中,下标元素i的左孩子为2i+1,右孩子为2i+2。下标元素为i的父节点在数组中的位置为(i-1)/2

大根堆:任何一棵子树的父节点都比子节点 的完全二叉树为大根堆

小根堆:任何一棵子树的父节点都比子节点 的完全二叉树为小根堆

堆排序的代码实现如下所示:

/**
 * 堆排序的过程
 * 1. 通过heapInsert函数依次将数组中前n个元素调整成大根堆结构
 * 2. 整个数组构成大根堆结构后,循环调用heapModify函数,
 *    heapModify函数的作用:每次将根节点与最后一个叶节点互换,
 *    则最大值就被排到了数组的末尾,再调整数组的前n-1个元素,使其满足大根堆结构
 */
public class HeapSort {
    public static void heapSort(int arr[]){
        int size = arr.length;
        if(size < 2){
            return ;
        }
        for(int i = 0; i < size; i++){
            heapInsert(arr, i);
        }
        swap(arr,0, --size);
        while(size > 0){
            heapModify(arr,0, size);
            swap(arr,0,--size);
        }
    }
    //往大根堆中加入元素的过程是向上调整,使其始终满足大根堆的结构
    public static void heapInsert(int[] arr, int index){
        int t = (index-1)/2;
        while(arr[index] > arr[t]){
            swap(arr, index, t);
            index = t;
            t = (index - 1) / 2;
        }
    }

    //向下调整
    public static void heapModify(int[] arr, int index, int size){
        int left = 2 * index + 1;
        //找出一个最值之后,通过swap(arr,0,--size)将其放在数组后面,再接着排序该最值之前的数组
        while(left < size){
            //注意:当右孩子下标left+1不小于size时,说明没有右孩子,largest只能选取left
            int largest = arr[left] < arr[left + 1] && left + 1 < size ? left + 1 : left;

            //如果有比根节点大的叶节点,交换
            largest = arr[largest] > arr[index] ? largest : index;
            if (largest == index) {
                System.out.println(largest);
                break;
            }
            swap(arr, largest, index);
            //以交换下来的根节点为根节点,循环查看其作为子堆的根节点是否满足大根堆的结构
            index = largest;
            left = index * 2 + 1;
        }
    }
    public static void swap(int[] arr, int a, int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    public static void printArray(int[] arr){
        for(int value: arr){
            System.out.print(value + " ");
        }
        System.out.println();
    }

    public static void main(String[] args){
        int[] arr = {0,5,3};
        heapSort(arr);
        printArray(arr);
    }
}

注意:heapModify函数中

正确形式:

int largest = arr[left] < arr[left + 1] && left + 1 < size ? left + 1 : left;

错误形式:

int largest = arr[left] > arr[left + 1] && left + 1 < size ? left : left + 1;

如果出现left + 1 == size的情况,largest选择左节点,而不是右节点。

4. 归并排序

归并排序也用了递归的思想,先把数组分成两个子部分,两个子部分递归排好序后,再合并到一起。子部分递归进行归并排序,在子部分合并过程中进行排序。

public class MergeSort {
    public static void mergeSort(int[] arr, int L, int R){
        if(L == R || arr == null){
            return;
        }
        int mid = (L + R) / 2;
        mergeSort(arr, L, mid);
        mergeSort(arr, mid + 1, R);
        merge(arr, L, mid, R);
    }

    //子部分合并的过程中进行排序。子部分其实就是两个有序的数组。在[L,mid]之间有序,在[mid+1,R]之间有序。
    public static void merge(int[] arr, int L, int mid, int R){
        //定义一个help数组,用于存放两个子部分合并之后的数组。合并结束后,help数组整体有序。
        int[] help = new int[R - L + 1];
        int p = 0;
        int i = L;
        int j = mid + 1;

        //如下while循环运行完成之后,最多只有一个子数组还没有完全合并到help数组中
        while(i <= mid && j <= R){
            help[p++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
        }

        //将剩下的数直接添加到help数组后面
        while(i <= mid){
            help[p++] = arr[i++];
        }
        while(j <= R){
            help[p++] = arr[j++];
        }

        //将help数组复制到arr数组。
        for(int k = 0; k < help.length; k++){
            arr[L + k] = help[k];
        }
    }

    public static void main(String[] args) {
        int[] a = {1,5,4,7,8,9,8,5,3,7,6,0,-4,10};
        mergeSort(a,0, a.length - 1);
        for(int i = 0; i < a.length; i++){
            System.out.println(a[i]);
        }
    }
}

套用master公式,其时间复杂度为 T ( N ) = 2 ∗ T ( N / 2 ) + O ( N ) T(N) = 2*T(N/2) + O(N) T(N)=2T(N/2)+O(N),即 O ( N ∗ l o g N ) O(N*log{N}) O(NlogN)
归并的空间复杂度就是那个临时的数组和递归时压入栈的数据占用的空间:n + logn;所以空间复杂度为: O(n)

小和问题

在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。

example:

[1,3,2,4]

  • 3左边比3小的数:1

  • 2左边比2小的数:1

  • 4左边比4小的数:1,3,2

小和=1+1+1+3+2= 8

转换一下思考方式,从最左边开始,一个数的右边有多少个比当前数大的数,就加几次

  • 1的右边有3个数比1大:1*3;
  • 3的右边有1个数比3大:3*1;
  • 2的右边有1个数比2大:2*1;

小和= 1 * 3 + 3 * 1+2=8

这个问题可以用归并排序来解决,因为每一个merge操作都会产生比较。每次比较中,较小的数计入总和。

如果两个数组合并,那么这两个数组肯定是有序的,比如[a,…]和[b,…],如果a<b,那么[b,…]中的所有数都大于a,假设[b,…]长度为L,则最终计入总和的值为a*L

用代码表达出来就是如下形式:

res += arr[i] < arr[j] ? arr[i] * (R - j + 1) : 0;

完整的代码如下:

package class_01;

public class SmallSum2 {
    public static int mergeSort(int[] arr, int L, int R){
        if(L == R || arr == null){
            return 0;
        }
        int mid = (L + R) / 2;
        return mergeSort(arr, L, mid)
                + mergeSort(arr, mid + 1, R)
                + merge(arr, L, mid, R);
    }

    public static int merge(int[] arr, int L, int mid, int R){
        int[] help = new int[R - L + 1];
        int p = 0;
        int i = L;
        int j = mid + 1;
        int res = 0;
        while(i <= mid && j <= R){
            res += arr[i] < arr[j] ? arr[i] * (R - j + 1) : 0;
            help[p++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
        }
        while(i <= mid){
            help[p++] = arr[i++];
        }
        while(j <= R){
            help[p++] = arr[j++];
        }
        for(int k = 0; k < help.length; k++){
            arr[L + k] = help[k];
        }
        return res;
    }

    public static void main(String[] args) {
        int[] a = {1,3,2,5,4,6};
        int result = mergeSort(a,0, a.length - 1);
        for(int i = 0; i < a.length; i++){
            System.out.println(a[i]);
        }
        System.out.println("result:" + result);
    }
}

与归并排序代码的区别就是:在merge操作的过程中返回result值。

小技巧:

  1. 求平均值,防止溢出的写法:int mid = L+(R-L)/2

    int mid = (L+R)/2,这样写的话,如果L和R都非常大,L+R有可能超过int的范围,就会产生溢出。

  2. 除以2的操作可以使用移位来完成,速度更快

    (a+b)/2可以写成(a+b)>>1

5. 非基于比较的排序

5.1 计数排序

5.2 桶排序

5.3 基数排序

6. 递归问题

剖析递归行为和递归行为复杂度的估算

寻找一个数组中最大值,使用递归的代码如下:

public class Recursion {
    public static int getMax(int[] arr, int l, int r){
        if(l == r){
            return arr[l];
        }
        int mid = (l + r) / 2;
        int leftResult = getMax(arr, l, mid);
        int rightResult = getMax(arr, mid + 1, r);
        return Math.max(leftResult, rightResult);
    }
    public static void main(String[] args) {
        int[] arr = {4,3,2,1};
        System.out.println(getMax(arr, 0, arr.length - 1));
    }
}

该递归函数,总样本数为N,每次递归将样本一分为二,分别求最大值。最后再比较两个最大值,返回最大的那一个。其算法复杂度可以用以下公式: T ( N ) = 2 T ( N / 2 ) + O ( 1 ) T(N) = 2 T(N/2) + O(1) T(N)=2T(N/2)+O(1)

显而易见,其算法复杂度为 O ( N ) O(N) O(N)

对于更复杂的递归算法,可以使用master公式

T ( N ) = a ∗ T ( N / b ) + o ( N d ) T(N) = a * T(N/b) + o(N^d) T(N)=aT(N/b)+o(Nd)

  • $\log(b, a)> d $ -> 复杂度为 O ( N log ⁡ ( b , a ) ) O(N^{\log(b,a)}) O(Nlog(b,a))
  • $\log(b, a)= d $ -> 复杂度为 O ( N d ∗ log ⁡ ( N ) ) O(N^d * \log(N)) O(Ndlog(N))
  • $\log(b, a)< d $ -> 复杂度为 O ( N d ) O(N^{d}) O(Nd)

例如,如果套用公式得到 T ( N ) = 2 T ( N / 2 ) + O ( N ) T(N) = 2 T(N/2) + O(N) T(N)=2T(N/2)+O(N),那么最后的算法复杂度为 O ( N ∗ log ⁡ N ) O(N*\log{N}) O(NlogN)

但是master公式只适用于一般情况,子递归过程不是平均划分样本量,就另当别算了。

T ( N ) = 2 ∗ T ( N / 4 ) + 3 ∗ T ( N / 6 ) + O ( N ) T(N) = 2 * T(N/4) + 3 * T(N/6)+ O(N) T(N)=2T(N/4)+3T(N/6)+O(N)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值