专题1:十大经典排序(转载)

参考转载:十大经典排序

0、排序算法说明(本文均小->大为例)

  • 0.1排序定义
    对一序列对象根据某个关键字进行排序。

  • 0.2术语说明
      1.稳定性 :如果a原本在b前面,而a=b,排序之后a仍然在b的前面则稳定,否则不稳定;
      2.内排序 :所有排序操作都在内存中完成;
      3.外排序 :排序通过磁盘和内存的数据传输才能进行(由于数据太大,因此把数据放在磁盘中);
      4.时间复杂度 : 一个算法执行所耗费的时间。
      5.空间复杂度 :运行完一个程序所需内存的大小。

  • 0.3 算法总结
    在这里插入图片描述
    其中:n: 数据规模;k: “桶”的个数;In-place: 占用常数内存,不占用额外内存;Out-place: 占用额外内存。
    简单排序算法有:1冒泡排序,2选择排序,3插入排序,4希尔排序。

  • 0.5 算法分类
    在这里插入图片描述

  • 0.6 比较和非比较的区别

    比较排序 :在排序的最终结果里,每个数都必须和其他数进行比较,才能确定自己的位置 。如快速排序、归并排序、堆排序、冒泡排序
     在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。
     比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。

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


1、冒泡排序(Bubble Sort)

 遍历要排序的数列,一次比较两个元素,如果顺序错误就交换。重复直到该数列排序完成。越小的元素会经由交换慢慢“浮”到数列的顶端。

  • 1.1 算法描述
     步骤1: 比较相邻元素。如果第一个比第二个大,交换;
     步骤2: 对每一对相邻元素作同样的工作,结果在最后的元素为最大数;
     步骤3: 每次除最后一个元素外,重复以上步骤直到排序完成。
  • 1.2 动图演示
    在这里插入图片描述
  • 1.3 代码实现
 public static int[] bubbleSort(int[] array) {
        if (array.length == 0)
            return array;
        for (int i = 0; i < array.length; i++)
            for (int j = 0; j < array.length - 1 - i; j++)
                if (array[j + 1] < array[j]) {
                    int temp = array[j + 1];
                    array[j + 1] = array[j];
                    array[j] = temp;
                }
        return array;
    }
  • 1.4 算法分析
    最佳情况:T(n) = O(n)
    最差情况:T(n) = O(n2)
    平均情况:T(n) = O(n2)

2、选择排序(Selection Sort)

 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。

  • 2.1 算法描述
     步骤1:初始状态:无序区为R[1…n],有序区为空;
     步骤2:第i趟排序(i=1~n-1)开始时,有序区和无序区分别为R[1…i-1]和R(i…n)。
        从当前无序区中选出关键字最小的记录 R[k],与无序区的第1个记录R交换。有序区记录个数+1,无序区个数-1;
     步骤3:n-1趟结束,数组有序化了。

  • 2.2 动图演示
    在这里插入图片描述

  • 2.3 代码实现

 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;
    }
  • 2.4 算法分析
    最佳情况:T(n) = O(n2)
    最差情况:T(n) = O(n2)
    平均情况:T(n) = O(n2)

用到它的时候,数据规模越小越好。
 优点:不占用额外的内存空间了,稳定。
 缺点:因为无论什么数据进去都是O(n2)的时间复杂度 。


3、插入排序(Insertion Sort)

 通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(只需O(1)的额外空间),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

  • 3.1 算法描述
      步骤1: 从第一个元素开始,该元素可以认为已经被排序;
      步骤2: 取出下一个元素,在已经排序的元素序列中从后向前扫描;
      步骤3: 如果该元素(已排序)大于新元素,将该元素移到下一位置;
      步骤4: 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
      步骤5: 将新元素插入到该位置后;
      步骤6: 重复步骤2~5。

  • 3.2 动图演示
    在这里插入图片描述

  • 3.3 代码实现
public static int[] insertionSort(int[] array) {
        if (array.length == 0)
            return array;
        int current;
        for (int i = 0; i < array.length - 1; i++) {
            current = array[i + 1];
            int preIndex = i;
            while (preIndex >= 0 && current < array[preIndex]) {
                array[preIndex + 1] = array[preIndex];
                preIndex--;
            }
            array[preIndex + 1] = current;
        }
        return array;
    }
  • 3.4 算法分析
    最佳情况:T(n) = O(n)
    最坏情况:T(n) = O(n2)
    平均情况:T(n) = O(n2)

4、希尔排序(Shell Sort)

 把记录按一定增量分组,对每组使用直接插入排序排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,排序完成。简单插入排序改进后的版本,能优先比较距离较远的元素,又叫缩小增量排序。

  • 4.1 算法描述
     选择增量gap=length/2(希尔增量,非最优),缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。
      步骤1:选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
      步骤2:按增量序列个数k,对序列进行k 趟排序;
      步骤3:每趟排序,根据对应的增量ti,将待排序列分割成若干子序列,对各子表进行直接插入排序。增量因子为1 时,排序完成。

  • 4.2 过程演示
    在这里插入图片描述

  • 4.3 代码实现

 public static int[] ShellSort(int[] array) {
        int len = array.length;
        int temp, gap = len / 2;
        while (gap > 0) {
            for (int i = gap; i < len; i++) {
                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;
    }
  • 4.4 算法分析
    最佳情况:T(n) = O(nlog2 n)
    最坏情况:T(n) = O(nlog2 n)
    平均情况:T(n) =O(nlog2n)

5、归并排序(Merge Sort)

 采用分治思想。将已有序的子序列合并,得到完全有序的序列。若将两个有序表合并成一个有序表,称为2-路归并。

  • 5.1 算法描述
     步骤1:把长度为n的输入序列分成两个长度为n/2的子序列;
     步骤2:对这两个子序列分别采用归并排序;
     步骤3:将两个排序好的子序列合并成一个最终的排序序列。

  • 5.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));
    }
  • 5.4 算法分析
    最佳情况:T(n) = O(n)
    最差情况:T(n) = O(nlogn)
    平均情况:T(n) = O(nlogn)

 归并排序与选择排序比,时间复杂度低,但需要额外的内存空间。


6、快速排序(Quick Sort)

 通过一趟排序将待排记录分隔成独立的两部分(分治思想),其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序,无稳定性典型应用:荷兰国旗问题。
 c语言中可直接调用qsort()函数,详见比较器-各个数据结构的应用(c语言中的qsort函数),工程上少于60的数量级且类型统一均可用快排。

  • 6.1 算法描述
      步骤1:从数列中挑出一个元素,称为 “基准”(pivot );
      步骤2:重新排序数列,所有元素比基准值小的摆放在基准前面,比基准值大摆后面(相同的数可以到任一边,相同值无差异)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
      步骤3:递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
  • 6.2 动图演示image
  • 6.3 代码实现
public static int[] QuickSort(int[] array, int start, int end) {
        if (array.length < 1 || start < 0 || end >= array.length || start > end) return null;
        int smallIndex = partition(array, start, end);
        if (smallIndex > start)
            QuickSort(array, start, smallIndex - 1);
        if (smallIndex < end)
            QuickSort(array, smallIndex + 1, end);
        return array;
}
public static int partition(int[] array, int start, int end) {
        int pivot = (int) (start + Math.random() * (end - start + 1));
        int smallIndex = start - 1;
        swap(array, pivot, end);
        for (int i = start; i <= end; i++)
            if (array[i] <= array[end]) {
                smallIndex++;
                if (i > smallIndex)
                    swap(array, i, smallIndex);
            }
        return smallIndex;
}
  • 6.4 算法分析
    最佳情况:T(n) = O(nlogn)
    最差情况:T(n) = O(n2)
    平均情况:T(n) = O(nlogn)

改进快排:随机快排复杂度O(N*log2N),O(N)额外空间。


7、堆排序(Heap Sort)

 利用堆这种结构所设计的一种算法。堆近似完全二叉树,子结点的键值或索引总是小于(或者大于)它的父节点。

  • 7.1 算法描述
     步骤1:将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
     步骤2:将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
     步骤3:交换后新堆顶R[1]可能违反堆性质,对当前无序区(R1,R2,……Rn-1)调整为新堆。重复直到有序区的元素个数为n-1,排序完成。

  • 7.2 动图演示image

  • 7.3 代码实现
    注意:这里用到了完全二叉树的部分性质:详情见数据结构二叉树知识点

//声明全局变量,用于记录数组array的长度;
    static int len;
    public static int[] HeapSort(int[] array) {
        len = array.length;
        if (len < 1) return array;
        //1.构建一个最大堆
        buildMaxHeap(array);
        //2.循环将堆首位(最大值)与末位交换,然后在重新调整最大堆
        while (len > 0) {
            swap(array, 0, len - 1);
            len--;
            adjustHeap(array, 0);
        }
        return array;
    }
    //建立最大堆
   
    public static void buildMaxHeap(int[] array) {
        //从最后一个非叶子节点开始向上构造最大堆
        //for循环这样写会更好一点:i的左子树和右子树分别2i+1和2(i+1)
        for (int i = (len/2- 1); i >= 0; i--) {
            adjustHeap(array, i);
        }
    }
    //调整使之成为最大堆
    
    public static void adjustHeap(int[] array, int i) {
        int maxIndex = i;
        //如果有左子树,且左子树大于父节点,则将最大指针指向左子树
        if (i * 2 < len && array[i * 2] > array[maxIndex])
            maxIndex = i * 2; 
        //如果有右子树,且右子树大于父节点,则将最大指针指向右子树
        if (i * 2 + 1 < len && array[i * 2 + 1] > array[maxIndex])
            maxIndex = i * 2 + 1; 
        //如果父节点不是最大值,则将父节点与最大值交换,并且递归调整与父节点交换的位置。
        if (maxIndex != i) {
            swap(array, maxIndex, i);
            adjustHeap(array, maxIndex);
        }
    }
  • 7.4 算法分析
    最佳情况:T(n) = O(nlogn)
    最差情况:T(n) = O(nlogn)
    平均情况:T(n) = O(nlogn)

建立大根堆复杂度O(N)=log21 + log22 + … + log2(N-1)


8、计数排序(Counting Sort)

 使用一个额外数组C,定义C[i] 为待排序数组A中值等于i的元素的个数。根据数组C重构A。要求输入的数据必须是有确定范围的整数。是一种稳定的排序算法。

  • 8.1 算法描述
     步骤1:找出待排序的数组中最大和最小的元素;
     步骤2:C[i] = A中值为i的元素出现次数;
     步骤3:对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
     步骤4:反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

  • 8.2 动图演示image

  • 8.3 代码实现

public static int[] CountingSort(int[] array) {
        if (array.length == 0) return array;
        int bias, min = array[0], 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];
        }
        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, i = 0;
        while (index < array.length) {
            if (bucket[i] != 0) {
                array[index] = i - bias;
                bucket[i]--;
                index++;
            } else
                i++;
        }
        return array;
    }
  • 8.4 算法分析
    最佳情况:T(n) = O(n+k)
    最差情况:T(n) = O(n+k)
    平均情况:T(n) = O(n+k)

    当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
    C的长度 =待排序数组的最大值-最小值+1,这使得计数排序对于数据范围很大的数组,需要大量时间和内存。


9、桶排序(Bucket Sort)

  假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶分别排序,再进行拼接。是计数排序的升级版,利用了函数的映射关系,高效与否在于映射函数的确定。桶可为任何类型,计数排序 和 基数排序均为其中的应用,是稳定的算法。工程上常用。

  • 9.1 算法描述
      步骤1:人为设置一个BucketSize,作为每个桶所能放置多少个不同数值(例如当BucketSize==5时,该桶可以存放{1,2,3,4,5}这几种数字,但是容量不限,即可以存放100个3);
      步骤2:遍历输入数据,并且把数据一个一个放到对应的桶里去;
      步骤3:对每个不是空的桶进行排序(可能使用别的排序算法 或 递归);
      步骤4:从不是空的桶里把排好序的数据拼接起来。

  注意,如果递归使用桶排序为各个桶排序,则当桶数量为1时要手动减小BucketSize增加下一循环桶的数量,否则会陷入死循环,导致内存溢出。

  • 9.2 图片演示
    image
  • 9.3 代码实现
public static ArrayList<Integer> BucketSort(ArrayList<Integer> array, int bucketSize) {
        if (array == null || array.size() < 2)
            return array;
        int max = array.get(0), min = array.get(0);
        // 找到最大值最小值
        for (int i = 0; i < array.size(); i++) {
            if (array.get(i) > max)
                max = array.get(i);
            if (array.get(i) < min)
                min = array.get(i);
        }
        int bucketCount = (max - min) / bucketSize + 1;
        ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
        ArrayList<Integer> resultArr = new ArrayList<>();
        for (int i = 0; i < bucketCount; i++) {
            bucketArr.add(new ArrayList<Integer>());
        }
        for (int i = 0; i < array.size(); i++) {
            bucketArr.get((array.get(i) - min) / bucketSize).add(array.get(i));
        }
        for (int i = 0; i < bucketCount; i++) {
            if (bucketSize == 1) { // 如果带排序数组中有重复数字时
                for (int j = 0; j < bucketArr.get(i).size(); j++)
                    resultArr.add(bucketArr.get(i).get(j));
            } else {
                if (bucketCount == 1)
                    bucketSize--;
                ArrayList<Integer> temp = BucketSort(bucketArr.get(i), bucketSize);
                for (int j = 0; j < temp.size(); j++)
                    resultArr.add(temp.get(j));
            }
        }
        return resultArr;
    }
  • 9.4 算法分析
    最佳情况:T(n) = O(n+k)
    最差情况:T(n) = O(n+k)
    平均情况:T(n) = O(n2)

    桶排序最好情况下使用线性时间O(n),取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。


10、基数排序(Radix Sort)

 按照低位先排序,收集;然后再按照高位排序,再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。是非比较的排序算法。

  • 10.1 算法描述
     步骤1:取得数组中的最大数,并取得位数;
     步骤2:arr为原始数组,从最低位开始取每个位组成radix数组;
     步骤3:对radix进行计数排序(利用计数排序适用于小范围数的特点);
  • 10.2 动图演示image
  • 10.3 代码实现
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;
    }
  • 10.4 算法分析
    最佳情况:T(n) = O(nk)
    最差情况:T(n) = O(nk)
    平均情况:T(n) = O(nk)
    对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数。

  • 10.5 基数排序有两种方法:
    MSD 从高位开始进行排序
    LSD 从低位开始进行排序

  • 基数排序 vs 计数排序 vs 桶排序
     这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
    1.基数排序: 根据键值的每位数字来分配桶
    2.计数排序: 每个桶只存储单一键值
    3.桶排序: 每个桶存储一定范围的数值


各种排序方法的选择:

通常需考虑的因素有:待排序的记录个数;记录本身的大小;记录的键值分布情况等。

  • 就平均时间性能而言,快速排序最佳,其所需时间最省,但快速排序在最坏情况下的时间性能不如堆排序和归并排序。当n较大时(>60),归并排序较堆排序省,但归并排序所需的辅助空间最大。
  • 简单排序方法中,直接插入排序最简单,当待排序的结点已按键值“基本有序”且n较小时,则应采用直接插入排序或冒泡排序,直接插入排序比冒泡排序更快些,因此经常将直接插入排序和其他的排序方法结合在一起使用。
  • 当n很大且键值位数较小时,采用基数排序较好;而当键值的最高位分布较均匀时,可先按其最高位将待排序结点分成若干子表,而后对各子表进行直接插入排序。
  • 从方法的稳定性来比较,直接插入排序、冒泡排序、归并排序和基数排序是稳定的排序方法;而直接选择排序、希尔排序、堆排序和快速排序都是不稳定的排序方法。
  • 若待排序的记录个数n较小时,可采用简单排序方法;若n 较大时,应采用快速排序或堆排序。
  • 若待排序的记录已基本有序,可采用起泡排序。
  • 文件局部有序或文件长度较小的情况下,选直接插入排序。

  当待排序列基本有序时,对冒泡排序来说,若最大关键字位于序列首部,则每趟排序仅能使其“下沉”一个位置,要使其下沉到底部仍需n-1趟排序,也即时间复杂度仍为O(n2)。而对简单选择排序来说,其比较次数与待排序列的初始状态无关;归并排序要求待排序列已经部分有序,而部分有序的含义是待排序列由若干有序的子序列组成,即每个子序列必须有序,并且其时间复杂度为O(n log2n);直接插入排序在待排序列基本有序时,每趟的比较次数大为降低,也即n-1趟比较的时间复杂度由O(n2)降至O(n)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值