十大排序算法总结

冒泡排序

最佳情况:T(n) = O(n) 最差情况:T(n) = O(n^2) 平均情况:T(n) = O(n^2)

// 可以利用一个flag,标记是否进行了swap操作,来适当加快排序速度
public static int[] bubbleSort(int[] array) {
        if (array.length == 0)
            return array;
        for (int i = 0; i < array.length; i++) {
        	boolean flag = true;
            for (int j = 0; j < array.length - 1 - i; j++) {
                if (array[j + 1] < array[j]) {
                    flag = false;
                    int temp = array[j + 1];
                    array[j + 1] = array[j];
                    array[j] = temp;
                }
            }
            if(flag == true)
            	return array;
        }
        return array;
    }

选择排序

最佳情况:T(n) = O(n^2) 最差情况:T(n) = O(n^2) 平均情况:T(n) = O(n^2)

​ 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

// 选择排序,第n趟排序后,一定至少有n个数字已经在目标位置上
    public static int[] selectionSort(int[] array) {
        if (array.length == 0)
            return array;
        for (int i = 0; i < array.length; i++) {
            int minIndex = i;
            for (int j = i; j < array.length; j++) {
                if (array[j] < array[minIndex]) //找到最小的数
                    minIndex = j; //将最小数的索引保存
            }
            int temp = array[minIndex];
            array[minIndex] = array[i];
            array[i] = temp;
        }
        return array;
    }

插入排序

在这里插入图片描述

最佳情况:T(n) = O(n) 最坏情况:T(n) = O(n^2) 平均情况:T(n) = O(n^2)

​ 通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

// 插入排序,将一个未排序的数字,依次与已经排序好的数进行比较,需要将已排序的数往后挪位
    public static int[] insertionSort(int[] array) {
        int len = array.length;
        // 基本情况下的数组可以直接返回
        if(array == null || len == 0 || len == 1) {
            return array;
        }
        for (int i = 0; i < len - 1; i++) {
            // 第一个数默认已排序,从第二个数开始
            int current = array[i + 1];
            // 前一个数的下标
            int preIdx = i;

            // 拿当前的数与之前已排序序列逐一往前比较,
            // 如果比较的数据比当前的大,就把该数往后挪一步
            while (preIdx >= 0 && current < array[preIdx]) {
                array[preIdx + 1] = array[preIdx];
                preIdx--;
            }
            // while循环跳出说明找到了位置
            array[preIdx + 1] = current;
        }
        return array;
    }

希尔排序

在这里插入图片描述

最佳情况:T(n) = O(nlog2 n) 最坏情况:T(n) = O(nlog2 n) 平均情况:T(n) =O(nlog2 n)

​ 在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。(同组中的元素采用插入排序)下图中,同一颜色在同一组

// 希尔排序(其中每组使用了插入排序)
    public static int[] ShellSort(int[] array){
        int len = array.length;
        int gap = len / 2;
        while(gap > 0){
            for(int i = gap; i < len; i++){
                int temp = array[i];
                int preIndex = i - gap;
                while(preIndex >= 0 && array[preIndex] > temp){
                    array[preIndex + gap] = array[preIndex];
                    preIndex -= gap;
                }
                array[preIndex + gap] = temp;
            }
            gap /= 2;
        }
        return array;
    }

归并排序

在这里插入图片描述

最佳情况:T(n) = O(n) 最差情况:T(n) = O(nlogn) 平均情况:T(n) = O(nlogn)

该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。
public static int[] MergeSort(int[] array) {
        if (array.length < 2) return array;
        int mid = array.length / 2;
        int[] left = Arrays.copyOfRange(array, 0, mid);
        int[] right = Arrays.copyOfRange(array, mid, array.length);
        return merge(MergeSort(left), MergeSort(right));
    }
    /**
     * 归并排序——将两段排序好的数组结合成一个排序数组
     */
    public static int[] merge(int[] left, int[] right) {
        int[] result = new int[left.length + right.length];
        for (int index = 0, i = 0, j = 0; index < result.length; index++) {
            if (i >= left.length)
                result[index] = right[j++];
            else if (j >= right.length)
                result[index] = left[i++];
            else if (left[i] > right[j])
                result[index] = right[j++];
            else
                result[index] = left[i++];
        }
        return result;
    }

快速排序

最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(nlogn)

  • 从数列中挑出一个元素,称为 “基准”(pivot)(下例挑选数组的第一个元素作为基准);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

注意partiotion函数中 i 、j 取值,以及while循环判断条件

    // 快速排序
    public static int[] quickSort(int[] a,int start, int end){
        if(start < end){                //至少两个位置
            int pivotIndex = partition(a, start, end);  // 定义pivotIndex中间位置。partition是检索这个方法
            quickSort(a, start ,pivotIndex - 1);              //排序左半边
            quickSort(a,pivotIndex + 1, end);                //排序右半边
        }
        return a;
    }
	public static  int partition(int[] a, int start, int end){  
    //对数组A下标从start到end中选一个主元,确定其位置,左边小于,右边大于。
        int pivot = a[start];  //先定义区间数组第一个元素为主元
        int i = start;   //定义最低的索引i是start。比主元大一位
        int j = end;     //定义最高的索引j是end
        while(i < j){   //当不等于high的位置时,执行以下循环
            while(a[j] > pivot && i < j)    //当 j的索引上的值比主元大时,且索引大于i时
                j--;                        //寻找比主元小的值的位置索引
            while(a[i] <= pivot && i < j)   //当i的索引上的值比主元小时,索引小于j时!! 注意等号!!!
                i++;                        //寻找比主元大的值的位置索引。
            if(i < j){   //交换low和high的值
                int temp = a[i];
                a[i] = a[j];
                a[j] = temp;
            }
        }
        a[start] = a[j];
        a[j] = pivot;
        return j;
    }

堆排序

为什么选择快速排序而不选择堆排序

  1. 堆排序访问数据的方式没有快速排序友好

  2. 对于同样的数据,在排序过程中,堆排序的数据交换次数要多于快速排序

最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(nlogn) 平均情况:T(n) = O(nlogn)

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

**(下标从0开始(0 ~ n-1))**完全二叉树序号为 i 的节点,其左子树序号为 2*i+1 (如果存在),右子数序号为 2*i+2

完全二叉树一共有n个节点,则最后一个非叶子节点的下标为 n/2 - 1 ,下标从0开始(0 ~ n-1)

	// 堆排序
    public static int[] HeapSort(int[] array) {
        int len = array.length;
        if (len < 1) return array;
        //1.构建一个最大堆
        //从最后一个非叶子节点开始向上构造最大堆
        for (int i = (len/2 - 1); i >= 0; i--) { //此处应该为 i = (len/2 - 1)
            adjustHeap(array, i, len);
        }
        //2.循环将堆首位(最大值)与末位交换,然后在重新调整最大堆
        while (len > 0) {

            int temp = array[len-1];
            array[len-1] = array[0];
            array[0] = temp;

            len--;
            adjustHeap(array, 0, len);
        }
        return array;
    }
    // 调整使之成为最大堆
    public static void adjustHeap(int[] array, int i, int len) {
        int maxIndex = i;
        //如果有左子树,且左子树大于父节点,则将最大指针指向左子树
        if (i * 2 + 1 < len && array[i * 2 + 1] > array[maxIndex])
            maxIndex = i * 2 + 1;
        //如果有右子树,且右子树大于父节点,则将最大指针指向右子树
        if (i * 2 + 2 < len && array[i * 2 + 2] > array[maxIndex])
            maxIndex = i * 2 + 2;
        //如果父节点不是最大值,则将父节点与最大值交换
        if (maxIndex != i) {
            int temp = array[i];
            array[i] = array[maxIndex];
            array[maxIndex] = temp;
			// 只要调整了某个节点,就需要重新将被调整节点下面再调整为大顶堆
            adjustHeap(array,maxIndex,len);
        }
    }

计数排序

在这里插入图片描述

最佳情况:T(n) = O(n+k) 最差情况:T(n) = O(n+k) 平均情况:T(n) = O(n+k)

  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
	// 计数排序
    public static int[] CountingSort(int[] array) {
        if (array.length == 0) return array;
        int min = array[0];
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max)
                max = array[i];
            if (array[i] < min)
                min = array[i];
        }
        int bias = 0 - min;
        int[] bucket = new int[max - min + 1];
        Arrays.fill(bucket, 0);
        for (int i = 0; i < array.length; i++) {
            bucket[array[i] + bias]++;
        }
        int index = 0;
        int i = 0;   // 标记桶的下标
        while (index < array.length) {
            if (bucket[i] != 0) {    // 当此桶还有元素
                array[index] = i - bias;
                bucket[i]--;       // 出桶
                index++;
            } else
                i++;
        }
        return array;
    }

桶排序

在这里插入图片描述

最佳情况:T(n) = O(n+k) 最差情况:T(n) = O(n+k) 平均情况:T(n) = O(n2)

和计数排序类似,桶排序也对输入数据作了某种假设,因此它的速度也很快。具体来说,计数排序假设了输入数据都属于一个小区间内的整数,而桶排序则假设输入数据是均匀分布的,即落入每个桶中的元素数量理论也是差不多的,不会出现很多数落入同一个桶内的情况。

桶排序中很重要的一步就是桶的设定了,我们必须根据输入元素的情况,选择一个恰当的 “ getBucketIndex ” 算法,使得输入元素能够正确的放入对应的桶内,且保证输入数据能够尽量均匀的放入不同的桶内。

最糟糕的情况下,即所有的数据都放入了一个桶内,桶内自排序算法为插入排序,那么其时间复杂度就为 O(n ^ 2) 了。

其次,我们可以发现,区间划分的越细,即桶的数量越多,理论上分到每个桶中的元素就越少,桶内数据的排序就越简单,其时间复杂度就越接近于线性。

极端情况下,就是区间小到只有1,即桶内只存放一种元素,桶内的元素不再需要排序,因为它们都是相同的元素,这时桶排序差不多就和计数排序一样了。

	// 桶排序
    public static float[] bucketSort(float[] arr) {
        // 新建一个桶的集合
        ArrayList<LinkedList<Float>> buckets = new ArrayList<LinkedList<Float>>();
        for (int i = 0; i < 10; i++) {
            // 新建一个桶,并将其添加到桶的集合中去。
            // 由于桶内元素会频繁的插入,所以选择 LinkedList 作为桶的数据结构
            buckets.add(new LinkedList<Float>());
        }
        // 将输入数据全部放入桶中并完成排序
        for (float data : arr) {
            int index = getBucketIndex(data);
            insertSort(buckets.get(index), data);
        }
        // 将桶中元素全部取出来并放入 arr 中输出
        int index = 0;
        for (LinkedList<Float> bucket : buckets) {
            for (Float data : bucket) {
                arr[index++] = data;
            }
        }
        return arr;
    }
    /**
     * 计算得到输入元素应该放到哪个桶内
     */
    public static int getBucketIndex(float data) {
        // 这里例子写的比较简单,仅使用浮点数的整数部分作为其桶的索引值
        // 实际开发中需要根据场景具体设计
        return (int) data;
    }
    /**
     * 我们选择插入排序作为桶内元素排序的方法 每当有一个新元素到来时,我们都调用该方法将其插入到恰当的位置
     */
    public static void insertSort(List<Float> bucket, float data) {
        ListIterator<Float> it = bucket.listIterator();
        while (it.hasNext()) {
            if (data <= it.next()) {
                it.previous(); // 把迭代器的位置偏移回上一个位置
                it.add(data); // 把数据插入到迭代器的当前位置
                return;       // 已插入数据,直接退出函数
            }
        }
        it.add(data); // 否则把数据插入到链表末端
    }

基数排序

最佳情况:T(n) = O(n * k) 最差情况:T(n) = O(n * k) 平均情况:T(n) = O(n * k)

类似于我们小时候比较数,会先从个数比,再比较十位,再比较百位…(没有则取0)

  • 取得数组中的最大数,取得其位数(最外层循环次数);
  • arr为原始数组,从最低位开始取每个位组成radix数组;
  • 对radix进行计数排序(利用计数排序适用于小范围数的特点);

在这里插入图片描述

	// 基数排序
    public static int[] RadixSort(int[] array) {
        if (array == null || array.length < 2)
            return array;
        // 1.先算出最大数的位数;
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            max = Math.max(max, array[i]);
        }
        int maxDigit = 0;
        while (max != 0) {
            max /= 10;
            maxDigit++;
        }
        int mod = 10, div = 1;
        ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
        for (int i = 0; i < 10; i++)
            bucketList.add(new ArrayList<Integer>());
        for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
            for (int j = 0; j < array.length; j++) {
                int num = (array[j] % mod) / div;
                bucketList.get(num).add(array[j]);
            }
            int index = 0;
            for (int j = 0; j < bucketList.size(); j++) {
                for (int k = 0; k < bucketList.get(j).size(); k++)
                    array[index++] = bucketList.get(j).get(k);
                bucketList.get(j).clear();
            }
        }
        return array;
    }

基数排序 vs 计数排序 vs 桶排序

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶
  • 计数排序:每个桶只存储单一键值
  • 桶排序:每个桶存储一定范围的数值

术语以性能比较

  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;

  • 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;

  • 内排序:所有排序操作都在内存中完成;

  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;

  • 时间复杂度: 一个算法执行所耗费的时间。

  • 空间复杂度:运行完一个程序所需内存的大小。

  • n: 数据规模

  • k: “桶”的个数

  • In-place: 占用常数内存,不占用额外内存

  • Out-place: 占用额外内存

在这里插入图片描述

在这里插入图片描述

比较排序和非比较排序的区别

​ 常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。

冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logn次,所以时间复杂度平均O(nlogn)
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。

计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。
​ 非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)

比较排序和非比较排序的区别

​ 常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。

冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logn次,所以时间复杂度平均O(nlogn)
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。

计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。
​ 非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)
非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。

参考链接:https://www.cnblogs.com/guoyaohua/p/8600214.html

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值