目录
一. 前言
二. 冒泡排序(Bubble Sort)
2.1. 概念
2.2. 算法步骤
2.3. 代码实现
三. 选择排序(Selection Sort)
3.1. 概念
3.2. 算法步骤
3.3. 代码实现
四. 插入排序(Insertion Sort)
4.1. 概念
4.2. 算法步骤
4.3. 代码实现
五. 希尔排序(Shell Sort)
5.1. 概念
5.2. 算法步骤
5.3. 代码实现
六. 归并排序(Merge Sort)
6.1. 概念
6.2. 算法步骤
6.3. 代码实现
七. 快速排序(Quick Sort)
7.1. 概念
7.2. 算法步骤
7.3. 代码实现
八. 堆排序(Heap Sort)
8.1. 概念
8.2. 算法步骤
8.3. 代码实现
九. 计数排序(Counting Sort)
9.1. 概念
9.2. 算法步骤
9.3. 代码实现
十. 桶排序(Bucket Sort)
10.1. 概念
10.2. 算法步骤
10.3. 代码实现
十一. 基数排序(Radix Sort)
11.1. 概念
11.2. 算法步骤
11.3. 代码实现
一. 前言
排序算法是《数据结构与算法》中最基本的算法之一。排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。
算法分类,十种常见排序算法可以分为两大类:
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
算法复杂度:
排序算法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | O() | O() | O(n) | O(1) | In-place | 稳定 |
选择排序 | O() | O() | O() | O(1) | In-place | 不稳定 |
插入排序 | O() | O() | O(n) | O(1) | In-place | 稳定 |
希尔排序 | O() | O() | O(n) | O(1) | In-place | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | Out-place | 稳定 |
快速排序 | O(n log n) | O() | O(n log n) | O(n log n) | In-place | 不稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | In-place | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | Out-place | 稳定 |
桶排序 | O(n+k) | O() | O(n) | O(n+k) | Out-place | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | Out-place | 稳定 |
名词解释:
- n:数据规模。
- k:“桶”的个数。
- In-place:占用常数内存,不占用额外内存。
- Out-place:占用额外内存。
关于时间复杂度,具体可参见《时间复杂度O(n)》:
- 平方阶(O())排序:各类简单排序,如直接插入、直接选择和冒泡排序。
- 线性对数阶(O(nlogn))排序:快速排序、堆排序和归并排序。
- O(n1+§)) 排序:§ 是介于 0 和 1 之间的常数。希尔排序。
- 线性阶(O(n))排序 基数排序,此外还有桶、箱排序。
关于稳定性:
- 稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面。
- 不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面。
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
二. 冒泡排序(Bubble Sort)
2.1. 概念
冒泡排序(Bubble Sort)重复地遍历排序数列,每次比较两个元素大小,如果大小顺序不对则进行交换,并一直重复遍历到排序数列的最后一位。是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。
2.2. 算法步骤
算法步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤 1~3,直到排序完成。
最优时间复杂度 | 最坏时间复杂度 | 稳定性 |
---|---|---|
O(n) | O( ) | 稳定 |
动图演示:
2.3. 代码实现
Java 实现:
/*** 冒泡排序: Java** @author 流华追梦*/
public class BubbleSort {/** 冒泡排序** 参数说明: * a -- 待排序的数组* n -- 数组的长度*/public static void bubbleSort1(int[] a, int n) {int i,j;for (i=n-1; i>0; i--) {// 将a[0...i]中最大的数据放在末尾for (j=0; j<i; j++) {if (a[j] > a[j+1]) {// 交换a[j]和a[j+1]int tmp = a[j];a[j] = a[j+1];a[j+1] = tmp;}}}}/** 冒泡排序(改进版)** 参数说明: * a -- 待排序的数组* n -- 数组的长度*/public static void bubbleSort2(int[] a, int n) {int i,j;int flag; // 标记for (i=n-1; i>0; i--) {flag = 0; // 初始化标记为0// 将a[0...i]中最大的数据放在末尾for (j=0; j<i; j++) {if (a[j] > a[j+1]) {// 交换a[j]和a[j+1]int tmp = a[j];a[j] = a[j+1];a[j+1] = tmp;flag = 1; // 若发生交换,则设标记为1}}if (flag==0)break; // 若没发生交换,则说明数列已有序。}}public static void main(String[] args) {int i;int[] a = {20,40,30,10,60,50};System.out.printf("before sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");bubbleSort1(a, a.length);//bubbleSort2(a, a.length);System.out.printf("after sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");}
}
Python 实现:
def bubbleSort(arr):for i in range(1, len(arr)):for j in range(0, len(arr)-i):if arr[j] > arr[j+1]:arr[j], arr[j + 1] = arr[j + 1], arr[j]return arrif __name__ == "__main__":num_list = [29,10,14,37,14,200, 3]print(bubbleSort(num_list))
三. 选择排序(Selection Sort)
3.1. 概念
选择排序(Selection Sort)是一种简单直观的排序算法,也是表现最稳定的排序算法之一,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处就是不占用额外的内存空间。
它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
3.2. 算法步骤
算法步骤:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
n 个记录的直接选择排序可经过 n-1 趟直接选择排序得到有序结果。具体算法步骤如下:
- 初始状态:无序区为 R[1...n],有序区为空;
- 第i趟排序 (i=1,2,3,...,n-1) 开始时,当前有序区和无序区分别为 R[1,...,i-1] 和 R(i,...,n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录 R 交换,使R[1,...,i] 和 R[i+1,...,n) 分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- n-1 趟结束,数组有序化了。
最优时间复杂度 | 最坏时间复杂度 | 稳定性 |
---|---|---|
O( n²) | O( n²) | 不稳定 |
动图演示:
3.3. 代码实现
Java 实现:
/*** 选择排序: Java** @author 流华追梦*/public class SelectionSort {/** 选择排序** 参数说明: * a -- 待排序的数组* n -- 数组的长度*/public static void selectSort(int[] a, int n) {int i; // 有序区的末尾位置int j; // 无序区的起始位置int min; // 无序区中最小元素位置for(i=0; i<n; i++) {min=i;// 找出"a[i+1] ... a[n]"之间的最小元素,并赋值给min。for(j=i+1; j<n; j++) {if(a[j] < a[min])min=j;}// 若min!=i,则交换 a[i] 和 a[min]。// 交换之后,保证了a[0] ... a[i] 之间的元素是有序的。if(min != i) {int tmp = a[i];a[i] = a[min];a[min] = tmp;}}}public static void main(String[] args) {int i;int[] a = {20,40,30,10,60,50};System.out.printf("before sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");selectSort(a, a.length);System.out.printf("after sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");}
}
Python 实现:
"""
将数组元素按从小到大的顺序排序,先编写一个用于找出数组中最小元素的函数,然后进行选择排序
"""
def find_smallest(arr):smallest = arr[0] # 用于存储最小值smallest_index = 0 # 用于存储最小元素索引for i in range(1, len(arr)):if arr[i] < smallest:smallest = arr[i]smallest_index = ireturn smallest_indexdef selection_sort(arr):new_arr = []for i in range(len(arr)):smallest = find_smallest(arr)new_arr.append(arr.pop(smallest))return new_arrdef selection_sort_1(arr):lenght = len(arr)for i in range(lenght-1):min_index = ifor j in range(i + 1, lenght-1):if arr[min_index] > arr[j]:arr[min_index], arr[j] = arr[j], arr[min_index]return arrif __name__ == "__main__":print(selection_sort([5, 3, 6, 2, 10]))print(selection_sort_1([5, 3, 6, 2, 10]))
四. 插入排序(Insertion Sort)
4.1. 概念
插入排序(Insertion Sort)的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了。只要打过扑克牌的人都应该能够秒懂,在发完牌后,整理牌时通常都是按从小到大或从大到小进行插入排序。
4.2. 算法步骤
算法步骤:
- 从第一个元素开始,设该元素已被排序;
- 遍历下一个元素,在已经排序的元素序列中从后向前遍历;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复 3. 直到找到已排序的元素小于或者等于新元素的值;
- 将新元素插入到该位置后;
- 重复 2-5。
最优时间复杂度 | 最坏时间复杂度 | 稳定性 |
---|---|---|
O(n) | O( n²) | 稳定 |
动图演示:
算法分析:
插入排序在实现上,通常采用 in-place 排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
4.3. 代码实现
Java 实现:
/*** 插入排序: Java** @author 流华追梦*/public class InsertionSort {/** 插入排序** 参数说明: * a -- 待排序的数组* n -- 数组的长度*/public static void insertSort(int[] a, int n) {int i, j, k;for (i = 1; i < n; i++) {//为a[i]在前面的a[0...i-1]有序区间中找一个合适的位置for (j = i - 1; j >= 0; j--)if (a[j] < a[i])break;//如找到了一个合适的位置if (j != i - 1) {//将比a[i]大的数据向后移int temp = a[i];for (k = i - 1; k > j; k--)a[k + 1] = a[k];//将a[i]放到正确位置上a[k + 1] = temp;}}}public static void main(String[] args) {int i;int[] a = {20,40,30,10,60,50};System.out.printf("before sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");insertSort(a, a.length);System.out.printf("after sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");}
}
Python 实现:
def insertion(arr):lenght = len(arr)for i in range(1, lenght):for j in range(i, 0, -1):if arr[j] < arr[j - 1]:arr[j - 1], arr[j] = arr[j], arr[j - 1]else:breakreturn arrif __name__ == "__main__":arr = [54, 26, 93, 17, 77, 31, 44, 55, 20]print(arr)arr1 = insertion(arr)print(arr1)
五. 希尔排序(Shell Sort)
5.1. 概念
1959年 Shell 发明,第一个突破 O(n²) 的排序算法,是插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
5.2. 算法步骤
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
算法步骤:
- 选择一个增量序列 t1,t2,…,tk,其中 ti>tj,tk=1;
- 按增量序列个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
算法分析:
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者 Robert Sedgewick 提出的。
动图演示:
待排序数组 10 个数据:
假设计算出的排序区间为 4 ,那么第一次比较应该是用第 5 个数据与第 1 个数据相比较:
调换后的数据为 [ 7,2,5,9,8,10,1,15,12,3 ],然后指针右移,第 6 个数据与第 2 个数据相比较:
指针右移,继续比较:
如果交换数据后,发现减去区间得到的位置还存在数据,那么继续比较,比如下面这张图,12 和 8 相比较,原地不动后,指针从 12 跳到 8 身上,继续减去区间发现前面还有一个下标为 0 的数据 7 ,那么 8 和 7 相比较:
比较完之后的效果是 7,8,12 三个数为有序排列:
当最后一个元素比较完之后,我们会发现大部分值比较大的数据都似乎调整到数组的中后部分了。
假设整个数组比较长的话,比如有 100 个数据,那么我们的区间肯定是四五十,调整后区间再缩小成一二十还会重新调整一轮,直到最后区间缩小为 1,就是真正的排序来了:
指针右移,继续比较:
重复步骤,即可完成排序。
5.3. 代码实现
Java 实现:
/*** 希尔排序: Java** @author 流华追梦*/public class ShellSort {/*** 希尔排序** 参数说明: * a -- 待排序的数组* n -- 数组的长度*/public static void shellSort1(int[] a, int n) {// gap为步长,每次减为原来的一半。for (int gap = n / 2; gap > 0; gap /= 2) {// 共gap个组,对每一组都执行直接插入排序for (int i = 0 ;i < gap; i++) {for (int j = i + gap; j < n; j += gap) {// 如果a[j] < a[j-gap],则寻找a[j]位置,并将后面数据的位置都后移。if (a[j] < a[j - gap]) {int tmp = a[j];int k = j - gap;while (k >= 0 && a[k] > tmp) {a[k + gap] = a[k];k -= gap;}a[k + gap] = tmp;}}}}}/*** 对希尔排序中的单个组进行排序** 参数说明: * a -- 待排序的数组* n -- 数组总的长度* i -- 组的起始位置* gap -- 组的步长** 组是"从i开始,将相隔gap长度的数都取出"所组成的!*/public static void groupSort(int[] a, int n, int i,int gap) {for (int j = i + gap; j < n; j += gap) {// 如果a[j] < a[j-gap],则寻找a[j]位置,并将后面数据的位置都后移。if (a[j] < a[j - gap]) {int tmp = a[j];int k = j - gap;while (k >= 0 && a[k] > tmp) {a[k + gap] = a[k];k -= gap;}a[k + gap] = tmp;}}}/*** 希尔排序** 参数说明: * a -- 待排序的数组* n -- 数组的长度*/public static void shellSort2(int[] a, int n) {// gap为步长,每次减为原来的一半。for (int gap = n / 2; gap > 0; gap /= 2) {// 共gap个组,对每一组都执行直接插入排序for (int i = 0 ;i < gap; i++)groupSort(a, n, i, gap);}}public static void main(String[] args) {int i;int a[] = {80,30,60,40,20,10,50,70};System.out.printf("before sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");shellSort1(a, a.length);// shellSort2(a, a.length);System.out.printf("after sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");}
}
Python 实现:
def shell_sort(self, data):length = len(data)gap = 1while gap < length:gap = gap * 3 + 1# 第一层循环是改变gap的值对列表进行分组while gap > 0:for i in range(gap, length):value = data[i]j = i - gapwhile j >= 0 and data[j] > value:data[j+gap] = data[j]j -= gapdata[j+gap] = valuegap = gap // 3return data
六. 归并排序(Merge Sort)
6.1. 概念
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
- 自下而上的迭代。
6.2. 算法步骤
算法步骤:
- 把长度为 n 的输入序列分成两个长度为 n/2 的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
算法分析:
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
例:首先
原序先通过一半一半的拆分,然后:
然后再一步一步的向上合并,在合并的过程中完成了排序,因为两个序列是排好序的,所以都从左往右,互相比较选择较小的那个数放入最后的序列,s 是原序列,所以在一开始会有与 len(s) 的比较。
动图演示:
6.3. 代码实现
Java 实现:
/*** 归并排序: Java** @author 流华追梦*/public class MergeSort {/** 将一个数组中的两个相邻有序区间合并成一个** 参数说明: * a -- 包含两个有序区间的数组* start -- 第1个有序区间的起始地址。* mid -- 第1个有序区间的结束地址。也是第2个有序区间的起始地址。* end -- 第2个有序区间的结束地址。*/public static void merge(int[] a, int start, int mid, int end) {int[] tmp = new int[end-start+1]; // tmp是汇总2个有序区的临时区域int i = start; // 第1个有序区的索引int j = mid + 1; // 第2个有序区的索引int k = 0; // 临时区域的索引while(i <= mid && j <= end) {if (a[i] <= a[j])tmp[k++] = a[i++];elsetmp[k++] = a[j++];}while(i <= mid)tmp[k++] = a[i++];while(j <= end)tmp[k++] = a[j++];// 将排序后的元素,全部都整合到数组a中。for (i = 0; i < k; i++)a[start + i] = tmp[i];tmp=null;}/** 归并排序(从上往下)** 参数说明: * a -- 待排序的数组* start -- 数组的起始地址* endi -- 数组的结束地址*/public static void mergeSortUp2Down(int[] a, int start, int end) {if(a==null || start >= end)return ;int mid = (end + start)/2;mergeSortUp2Down(a, start, mid); // 递归排序a[start...mid]mergeSortUp2Down(a, mid+1, end); // 递归排序a[mid+1...end]// a[start...mid] 和 a[mid...end]是两个有序空间,// 将它们排序成一个有序空间a[start...end]merge(a, start, mid, end);}/** 对数组a做若干次合并: 数组a的总长度为len,将它分为若干个长度为gap的子数组;* 将"每2个相邻的子数组" 进行合并排序。** 参数说明: * a -- 待排序的数组* len -- 数组的长度* gap -- 子数组的长度*/public static void mergeGroups(int[] a, int len, int gap) {int i;int twolen = 2 * gap; // 两个相邻的子数组的长度// 将"每2个相邻的子数组" 进行合并排序。for(i = 0; i+2*gap-1 < len; i+=(2*gap))merge(a, i, i+gap-1, i+2*gap-1);// 若 i+gap-1 < len-1,则剩余一个子数组没有配对。// 将该子数组合并到已排序的数组中。if ( i+gap-1 < len-1)merge(a, i, i + gap - 1, len - 1);}/** 归并排序(从下往上)** 参数说明: * a -- 待排序的数组*/public static void mergeSortDown2Up(int[] a) {if (a==null)return ;for(int n = 1; n < a.length; n*=2)mergeGroups(a, a.length, n);}public static void main(String[] args) {int i;int[] a = {80,30,60,40,20,10,50,70};System.out.printf("before sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");mergeSortUp2Down(a, 0, a.length-1); // 归并排序(从上往下)// mergeSortDown2Up(a); // 归并排序(从下往上)System.out.printf("after sort:");for (i = 0; i < a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");}
}
Python 实现:
def merge(s1,s2,s):"""将两个列表是s1,s2按顺序融合为一个列表s,s为原列表"""# j和i就相当于两个指向的位置,i指s1,j指s2i = j = 0while i+j<len(s):# j==len(s2)时说明s2走完了,或者s1没走完并且s1中该位置是最小的if j==len(s2) or (i<len(s1) and s1[i]<s2[j]):s[i+j] = s1[i]i += 1else:s[i+j] = s2[j]j += 1def merge_sort(s):"""归并排序"""n = len(s)# 剩一个或没有直接返回,不用排序if n < 2:return# 拆分mid = n // 2s1 = s[0:mid]s2 = s[mid:n]# 子序列递归调用排序merge_sort(s1)merge_sort(s2)# 合并merge(s1,s2,s)if __name__ == '__main__':s = [1,7,3,5,4]merge_sort(s)print(s)
七. 快速排序(Quick Sort)
7.1. 概念
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(n log n) 次比较。在最坏状况下则需要 Ο(n²) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。快速排序使用分治法(Divide and conquer)策略来把一个串行 list 分为两个子串行 sub-lists。
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,查了很多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),
且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。
所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
7.2. 算法步骤
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法步骤如下:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
动图演示:
7.3. 代码实现
Java 实现:
/*** 快速排序: Java** @author 流华追梦*/
public class QuickSort {/** 快速排序** 参数说明: * a -- 待排序的数组* l -- 数组的左边界(例如,从起始位置开始排序,则l=0)* r -- 数组的右边界(例如,排序截至到数组末尾,则r=a.length-1)*/public static void quickSort(int[] a, int l, int r) {if (l < r) {int i,j,x;i = l;j = r;x = a[i];while (i < j) {while(i < j && a[j] > x)j--; // 从右向左找第一个小于x的数if(i < j)a[i++] = a[j];while(i < j && a[i] < x)i++; // 从左向右找第一个大于x的数if(i < j)a[j--] = a[i];}a[i] = x;quickSort(a, l, i-1); // 递归调用quickSort(a, i+1, r); // 递归调用}}public static void main(String[] args) {int i;int[] a = {30,40,60,10,20,50};System.out.printf("before sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");quickSort(a, 0, a.length-1);System.out.printf("after sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");}
}
Python 实现:
import randomdef quick_sort(arr):if len(arr) < 2:return arr #基线条件:为空或者只包括一个元素的数组是“有序”的else:pivot = arr[0] # 递归条件less = [i for i in arr[1:] if i <= pivot] # 由所有小于基准值的元素组成的子数组greater = [i for i in arr[1:] if i > pivot] # 由所有小于基准值得元素组成的子数组return quick_sort(less) + [pivot] + quick_sort(greater)def random_quick_sort(arr):if len(arr) < 2:return arrelse:pivot = random.randint(0, len(arr)-1)less = [i for i in arr[:pivot] +arr[pivot+1:] if i <= arr[pivot]]greater = [i for i in arr[:pivot] + arr[pivot+1:] if i > arr[pivot]]return random_quick_sort(less) + [arr[pivot]] + random_quick_sort(greater)print(quick_sort([2, 3, 5, 1, 8, 1, 9, 4, 7]))print(random_quick_sort([2, 3, 5, 1, 8, 1, 9, 4, 7]))
八. 堆排序(Heap Sort)
8.1. 概念
堆排序(Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。关于二叉树的结构的详细介绍,可以参见《二叉树、完全二叉树、完满二叉树、满二叉树》。
堆排序的平均时间复杂度为 Ο(nlogn)。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列。
8.2. 算法步骤
算法步骤:
- 将初始待排序关键字序列(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,则整个排序过程完成。
动画演示:
8.3. 代码实现
Java 实现:
/*** 堆排序: Java** @author 流华追梦*/
public class HeapSort {/* * (最大)堆的向下调整算法** 注: 数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。* 其中,N为数组下标索引值,如数组中第1个数对应的N为0。** 参数说明: * a -- 待排序的数组* start -- 被下调节点的起始位置(一般为0,表示从第1个开始)* end -- 截至范围(一般为数组中最后一个元素的索引)*/public static void maxHeapDown(int[] a, int start, int end) {int c = start; // 当前(current)节点的位置int l = 2*c + 1; // 左(left)孩子的位置int tmp = a[c]; // 当前(current)节点的大小for (; l <= end; c=l,l=2*l+1) {// "l"是左孩子,"l+1"是右孩子if ( l < end && a[l] < a[l+1])l++; // 左右两孩子中选择较大者,即m_heap[l+1]if (tmp >= a[l])break; // 调整结束else { // 交换值a[c] = a[l];a[l]= tmp;}}}/** 堆排序(从小到大)** 参数说明: * a -- 待排序的数组* n -- 数组的长度*/public static void heapSortAsc(int[] a, int n) {int i,tmp;// 从(n/2-1) --> 0逐次遍历。遍历之后,得到的数组实际上是一个(最大)二叉堆。for (i = n / 2 - 1; i >= 0; i--)maxHeapDown(a, i, n-1);// 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素for (i = n - 1; i > 0; i--) {// 交换a[0]和a[i]。交换后,a[i]是a[0...i]中最大的。tmp = a[0];a[0] = a[i];a[i] = tmp;// 调整a[0...i-1],使得a[0...i-1]仍然是一个最大堆。// 即,保证a[i-1]是a[0...i-1]中的最大值。maxHeapDown(a, 0, i-1);}}/* * (最小)堆的向下调整算法** 注: 数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。* 其中,N为数组下标索引值,如数组中第1个数对应的N为0。** 参数说明: * a -- 待排序的数组* start -- 被下调节点的起始位置(一般为0,表示从第1个开始)* end -- 截至范围(一般为数组中最后一个元素的索引)*/public static void minHeapDown(int[] a, int start, int end) {int c = start; // 当前(current)节点的位置int l = 2*c + 1; // 左(left)孩子的位置int tmp = a[c]; // 当前(current)节点的大小for (; l <= end; c=l,l=2*l+1) {// "l"是左孩子,"l+1"是右孩子if ( l < end && a[l] > a[l+1])l++; // 左右两孩子中选择较小者if (tmp <= a[l])break; // 调整结束else { // 交换值a[c] = a[l];a[l]= tmp;}}}/** 堆排序(从大到小)** 参数说明: * a -- 待排序的数组* n -- 数组的长度*/public static void heapSortDesc(int[] a, int n) {int i,tmp;// 从(n/2-1) --> 0逐次遍历每。遍历之后,得到的数组实际上是一个最小堆。for (i = n / 2 - 1; i >= 0; i--)minHeapDown(a, i, n-1);// 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素for (i = n - 1; i > 0; i--) {// 交换a[0]和a[i]。交换后,a[i]是a[0...i]中最小的。tmp = a[0];a[0] = a[i];a[i] = tmp;// 调整a[0...i-1],使得a[0...i-1]仍然是一个最小堆。// 即,保证a[i-1]是a[0...i-1]中的最小值。minHeapDown(a, 0, i-1);}}public static void main(String[] args) {int i;int[] a = {20,30,90,40,70,110,60,10,100,50,80};System.out.printf("before sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");heapSortAsc(a, a.length); // 升序排列// heapSortDesc(a, a.length); // 降序排列System.out.printf("after sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");}
}
Python 实现:
# heap_sort 代码实现def build(arr:List[int], root, end):while True:child = 2 * root + 1 # 左子节点的位置if child > end: # 若左子节点超过了最后一个节点,则终止循环breakif (child + 1 <= end) and (arr[child + 1] > arr[child]): # 若右子节点在最后一个节点之前,并且右子节点比左子节点大,则我们的孩子指针移到右子节点上child += 1if arr[child] > arr[root]: # 若最大的孩子节点大于根节点,则交换两者顺序,并且将根节点指针,移到这个孩子节点上arr[child], arr[root] = arr[root], arr[child]root = childelse:breakdef heap_sort(arr:List[int]):n = len(arr)first_root = n // 2 - 1 # 确认最深最后的那个根节点的位置for root in range(first_root, -1, -1): # 由后向前遍历所有的根节点,建堆并进行调整build(arr, root, n - 1)for end in range(n - 1, 0, -1): # 调整完成后,将堆顶的根节点与堆内最后一个元素调换位置,此时为数组中最大的元素,然后重新调整堆,将最大的元素冒到堆顶。依次重复上述操作arr[0], arr[end] = arr[end], arr[0]build(arr, 0, end - 1)# 测试数据
if __name__ == '__main__':import randomrandom.seed(54)arr = [random.randint(0,100) for _ in range(10)]print("原始数据:", arr)heap_sort(arr)print("堆排序结果:", arr)'''
# 输出结果原始数据: [17, 56, 71, 38, 61, 62, 48, 28, 57, 42]
堆排序结果: [17, 28, 38, 42, 48, 56, 57, 61, 62, 71]
'''
九. 计数排序(Counting Sort)
9.1. 概念
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
9.2. 算法步骤
算法步骤:
-
找出待排序的数组中最大和最小的元素;
-
统计数组中每个值为i的元素出现的次数,存入数组 C 的第 i 项;
-
对所有的计数累加(从 C 中的第一个元素开始,每一项和前一项相加);
-
反向填充目标数组:将每个元素 i 放在新数组的第 C(i) 项,每放一个元素就将 C(i) 减去 1。
算法分析:
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是 O(n+k),其排序速度快于任何比较排序算法。当 k 不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
动画演示:
9.3. 代码实现
Java 实现:
/*** 计数排序:Java* * @author 流华追梦*/
public class CountingSort {/*** 计数排序* * @param array*/public static void countSort(int[] array) {int max = array[0];//找出最大值for (int i = 1; i < array.length; i++) {if (max < array[i]) {max = array[i];}}//初始化计数数组int[] countArray = new int[max + 1];for (int i = 0; i < array.length; i++) {countArray[array[i]]++;}//求出每个元素的位置int index = 0;for (int i = 0; i <= max; i++) {while (countArray[i] > 0) {array[index++] = i;countArray[i]--;}}}public static void main(String[] args) {long startTime = System.currentTimeMillis();int[] sorted = new int[]{12,52,52,1,45,2,6,5,4,1};// 计数排序效率测试CountingSort.countSort(sorted);// 排序耗费的时间为:1msSystem.out.println("排序耗费的时间为:" + (System.currentTimeMillis() - startTime) + "ms");}
}
Python 实现:
# --*-- coding: utf-8 --*--
def CountingSort(arr):#检查入参类型if not isinstance(arr,(list)):raise TypeError('error para type')#获取arr中的最大值和最小值maxNum=max(arr)minNum=min(arr)#以最大值和最小值的差作为中间数组的长度,并构建中间数组,初始化为0length=maxNum-minNum+1tempArr=[0 for i in range(length)]#创建结果List,存放排序完成的结果resArr=list(range(len(arr)))#第一次循环遍历for num in arr:tempArr[num-minNum]+=1#第二次循环遍历for j in range(1,length):tempArr[j]=tempArr[j]+tempArr[j-1]#第三次循环遍历for i in range(len(arr)-1,-1,-1):resArr[tempArr[arr[i]-minNum]-1]=arr[i]tempArr[arr[i]-minNum]-=1return resArrif __name__=='__main__':arr=[12,25,26,13,14,25,12,17,18,14]print(CountingSort(arr))
十. 桶排序(Bucket Sort)
10.1. 概念
桶排序(Bucket Sort)是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。
为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中。同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
什么时候最快?当输入的数据可以均匀的分配到每一个桶中。
什么时候最慢?当输入的数据被分配到了同一个桶中。
10.2. 算法步骤
算法步骤:
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
算法分析:
桶排序最好情况下使用线性时间 O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为 O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
图片演示:
10.3. 代码实现
Java 实现:
/*** 桶排序: Java** @author 流华追梦*/
public class BucketSort {/** 桶排序** 参数说明: * a -- 待排序数组* max -- 数组a中最大值的范围*/public static void bucketSort(int[] a, int max) {int[] buckets;if (a==null || max<1)return ;// 创建一个容量为max的数组buckets,并且将buckets中的所有数据都初始化为0。buckets = new int[max];// 1. 计数for(int i = 0; i < a.length; i++) buckets[a[i]]++; // 2. 排序for (int i = 0, j = 0; i < max; i++) {while( (buckets[i]--) >0 ) {a[j++] = i;}}buckets = null;}public static void main(String[] args) {int i;int[] a = {8,2,3,4,3,6,6,3,9};System.out.printf("before sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");bucketSort(a, 10); // 桶排序System.out.printf("after sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");}
}
Python 实现:
def bucktetSort(numList,bucketNum):if len(numList) == 0 or len(numList) == 1:return numListmaxNum = numList[0]minNum = numList[0]for i in range(1,len(numList)): # 找到最大最小值if numList[i] < minNum:minNum = numList[i]elif numList[i] > maxNum:maxNum = numList[i]else:continuebucketSize = (maxNum - minNum + 1) // bucketNum # 根据桶的数量找到每个桶的范围buckets = [[] for i in range(bucketNum)]for i in range(len(numList)): # 将各个数分配到各个桶buckets[(numList[i] - minNum) // bucketSize].append(numList[i])for i in range(bucketNum): # 桶内排序,可以使用各种排序方法buckets[i].sort()res = []for i in range(len(buckets)): # 分别将各个桶内的数提出来,压入结果for j in range(len(buckets[i])):res.append(buckets[i][j])return resnumlist = [11,34,23,56,8,20,66,45,54,87,78]
print(bucktetSort(numlist,5)) # 利用5个桶
# 输出结果为:[8, 11, 20, 23, 34, 45, 54, 56, 66, 78, 87]
十一. 基数排序(Radix Sort)
11.1. 概念
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
基数排序、计数排序、桶排序这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值。
11.2. 算法步骤
算法步骤:
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点)。
算法分析:
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要 O(n) 的时间复杂度,而且分配之后得到新的关键字序列又需要 O(n) 的时间复杂度。假如待排数据可以分为 d 个关键字,则基数排序的时间复杂度将是 O(d*2n) ,当然 d 要远远小于 n,因此基本上还是线性级别的。
基数排序的空间复杂度为 O(n+k),其中 k 为桶的数量。一般来说 n>>k,因此额外空间需要大概 n个左右。
动图演示:
11.3. 代码实现
Java 实现:
/*** 基数排序: Java** @author 流华追梦*/
public class RadixSort {/** 获取数组a中最大值** 参数说明: * a -- 数组* n -- 数组长度*/private static int getMax(int[] a) {int max;max = a[0];for (int i = 1; i < a.length; i++)if (a[i] > max)max = a[i];return max;}/** 对数组按照"某个位数"进行排序(桶排序)** 参数说明: * a -- 数组* exp -- 指数。对数组a按照该指数进行排序。** 例如,对于数组a={50, 3, 542, 745, 2014, 154, 63, 616};* (01) 当exp=1表示按照"个位"对数组a进行排序* (02) 当exp=10表示按照"十位"对数组a进行排序* (03) 当exp=100表示按照"百位"对数组a进行排序* ...*/private static void countSort(int[] a, int exp) {//int output[a.length]; // 存储"被排序数据"的临时数组int[] output = new int[a.length]; // 存储"被排序数据"的临时数组int[] buckets = new int[10];// 将数据出现的次数存储在buckets[]中for (int i = 0; i < a.length; i++)buckets[ (a[i]/exp)%10 ]++;// 更改buckets[i]。目的是让更改后的buckets[i]的值,是该数据在output[]中的位置。for (int i = 1; i < 10; i++)buckets[i] += buckets[i - 1];// 将数据存储到临时数组output[]中for (int i = a.length - 1; i >= 0; i--) {output[buckets[ (a[i]/exp)%10 ] - 1] = a[i];buckets[ (a[i]/exp)%10 ]--;}// 将排序好的数据赋值给a[]for (int i = 0; i < a.length; i++)a[i] = output[i];output = null;buckets = null;}/** 基数排序** 参数说明: * a -- 数组*/public static void radixSort(int[] a) {int exp; // 指数。当对数组按各位进行排序时,exp=1;按十位进行排序时,exp=10;...int max = getMax(a); // 数组a中的最大值// 从个位开始,对数组a按"指数"进行排序for (exp = 1; max/exp > 0; exp *= 10)countSort(a, exp);}public static void main(String[] args) {int i;int[] a = {53, 3, 542, 748, 14, 214, 154, 63, 616};System.out.printf("before sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");radixSort(a); // 基数排序System.out.printf("after sort:");for (i=0; i<a.length; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");}
}
Python 实现:
def radix_sort(array):bucket, digit = [[]], 0while len(bucket[0]) != len(array):print ("bucket[0]:",bucket[0])bucket = [[], [], [], [], [], [], [], [], [], []]for i in range(len(array)):num = (array[i] // 10 ** digit) % 10bucket[num].append(array[i])array=[]for i in range(len(bucket)):array += bucket[i]print (digit,":",array)digit += 1return arrayprint (radix_sort([3,4,2,1,6,10,11,101,99,88]))