0、算法概述 0.1 算法分类 十种常见排序算法可以分为两大类:
比较类排序 :通过比较来决定元素间的相对次序,由于其时间复杂度不能突破 O(nlogn),因此也称为非线性时间比较类排序。
非比较类排序 :不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
0.2 算法复杂度
0.3 相关概念
稳定 :如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面。
不稳定 :如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度 :对排序数据的总的操作次数。反映当 n 变化时,操作次数呈现什么规律。
空间复杂度: 是指算法在计算机内执行时所需存储空间的度量,它也是数据规模 n 的函数。
1、冒泡排序(Bubble Sort) 冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢”浮”到数列的顶端。
1.1 算法描述
比较相邻的元素。如果第一个比第二个大,就交换它们两个;
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
针对所有的元素重复以上的步骤,除了最后一个;
重复步骤 1~3,直到排序完成。
1.2 动图演示
1.3 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void BubbleSort::sort (std::vector<int >& arr) { bool swapped; for (size_t i = 0 ; i < arr.size (); ++i) { swapped = false ; for (size_t j = 0 ; j < arr.size () - 1 - i; ++j) { if (arr[j] > arr[j + 1 ]) { std::swap (arr[j], arr[j + 1 ]); swapped = true ; } } if (!swapped) break ; } }
1.4 算法分析
2、插入排序(Insertion Sort) 插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
2.1 算法描述
每次将一个待排序的记录按其关键字大小插入到前面已经排好序的子序列中,直到全部的记录插入完成。
2.2 动图演示
2.2 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 void InsertionSort::sort (std::vector<int >& arr) { int len = arr.size (); for (int i=1 ;i<len;i++){ int temp = arr[i]; int j = 0 ; while (j<i && temp>=arr[j]) j++; for (int l=i;l>j;l--) {arr[l] = arr[l-1 ];} arr[j] = temp; } }void InsertionSort::sort (std::vector<int > &arr) { int len = arr.size (); for (int i = 1 ; i < len; i++) { int temp = arr[i]; int high = i,low = 0 ; while (low<=high){ int mid = (high + low)/2 ; if (arr[mid]>temp) high = mid-1 ; else if (arr[mid]==temp) { low = mid; break ; } else low = mid+1 ; } for (int l = i; l > low; l--)arr[l] = arr[l - 1 ]; arr[low] = temp; } }
2.4 算法分析 插入排序在实现上,通常采用 in-place 排序(即只需用到 O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
3、选择排序(Selection Sort) 选择排序(Selection-sort)是一种简单直观的排序算法。
3.1 算法描述 它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
3.2 动图演示
3.3 代码实现 1 2 3 4 5 6 7 8 9 10 void SelectionSort::sort (std::vector<int >& arr) { for (int i=0 ;i<arr.size ();i++){ int min = i; for (int j=i;j<arr.size ();j++) if (arr[j]<arr[min]) min = j; if (min!=i) swap (arr[i],arr[min]); } }
3.4 算法分析
适用性:顺序表 | 链表
4、希尔排序(Shell Sort) 1959 年 Shell 发明,第一个突破 O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。
希尔排序又叫 缩小增量排序 ,“先追求表中元素 部分有序,再逐渐逼近 全局有序”。
4.1 算法描述 先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
选择一个增量序列 t1,t2,…,tk,其中 ti>tj,tk=1;
按增量序列个数 k,对序列进行 k 趟排序;
每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
4.2 动图演示
4.3 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void ShellSort::sort (std::vector<int >& arr) { int i=0 ,j=0 ,len = arr.size (); int d = len/2 ; while (d>=1 ) { for (i=1 ; i<len; i++) { int temp = arr[i]; for (j=i-1 ; j>=0 ; j--) { if (temp<arr[j]){ arr[j+1 ]=arr[j]; }else break ; } arr[j+1 ] = temp; } d/=2 ; }
4.4 算法分析
5、快速排序(Quick Sort) 快速排序(Quick Sort)使用分治法策略。 它的基本思想是:选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
5.1 算法描述 快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
(1) 从数列中挑出一个基准值。
(2) 将所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边);在这个分区退出之后,该基准就处于数列的中间位置。
(3) 递归地把”基准值前面的子数列”和”基准值后面的子数列”进行排序。
5.2 动图演示
5.3 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #include "QuickSort.h" void QuickSort::sort (std::vector<int > &arr) { quickSort (arr, 0 , arr.size () - 1 ); }std::string QuickSort::name () const { return "QuickSort" ; }void QuickSort::quickSort (std::vector<int > &arr, int low, int high) { if (low < high) { int pivot = partition (arr, low, high); quickSort (arr, low, pivot - 1 ); quickSort (arr, pivot + 1 , high); } }int QuickSort::partition (std::vector<int > &arr, int low, int high) { int pivot = arr[low]; while (low < high) { while (arr[high] >= pivot && low < high) high--; arr[low] = arr[high]; while (arr[low] <= pivot && low < high) low++; arr[high] = arr[low]; } arr[low] = pivot; return low; }
5.4 算法分析
每一层的QuickSort只需要处理剩余的待排序元素,时间复杂度不超过$O(n)$
优化:尽可能取的pivot能够正好将数据中分
在头、中、尾元素中,取中间值
随机选择一个元素作为pivot
6、堆排序(Heap Sort) 什么是堆(heap) 堆(heap)
是一种满足特定条件的完全二叉树,主要分为两类:
小顶堆(min heap)
任意节点的值 ≤ 其子节点的值;
大顶堆(max heap)
任意节点的值 ≥ 其子节点的值;
堆作为完全二叉树的一个特列,有以下特性:
最底层的节点靠左填充,其他层的节点都被填满;
二叉树的根节点被称为堆顶
,最底层靠右的节点称为堆底
;
对于大顶堆(小顶堆),堆顶元素(根节点)的值是最大(最小)的;
【补充】完全二叉树的性质
假设完全二叉树中有n
个节点
最后一个非叶子节点 —— $ \lfloor n/2 \rfloor$
第一个叶子节点 ——$ \lfloor n/2 \rfloor+1 $
假设 i
为其中的某一个非叶子节点:
i
的左孩子 —— $2i$
i
的右孩子 —— $2i+1$
i
的父节点 —— $\lfloor i/2 \rfloor$
i
所在的层数 —— $ \lfloor log_2(i) \rfloor +1$ 或 $\lceil log_2(i+1) \rceil$
假设完全二叉树中有n
个节点,i
为其中的某一个非叶子节点:
判断i
是否有左孩子 —— $2i ≤n$?
判断i
是否有左孩子 —— $2i+1≤n$?
判断i
是否有分支节点 —— $i>\lfloor n/2 \rfloor$?
6.1 算法描述
6.2 动图演示
6.3 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include "HeapSort.h" void HeapSort::sort (std::vector<int > &arr) { int n = arr.size (); buildMaxHeap (arr, n); for (int i = n - 1 ; i > 0 ; --i) { swap (arr[0 ], arr[i]); heapify (arr, i, 0 ); } }void HeapSort::buildMaxHeap (std::vector<int > &arr, int n) { for (int i = n / 2 - 1 ; i >= 0 ; --i) heapify (arr, n, i); }void HeapSort::heapify (std::vector<int > &arr, int n, int i) { int max = i; int left_son = 2 * i + 1 ; int right_son = 2 * i + 2 ; if (left_son < n && arr[left_son] > arr[max]) max = left_son; if (right_son < n && arr[right_son] > arr[max]) max = right_son; if (max != i) { swap (arr[i], arr[max]); heapify (arr, n, max); } }std::string HeapSort::name () const { return "HeapSort" ; }
6.4 算法分析(以大顶堆为例) 时间复杂度
buildMaxHeap(int &arr, int n)
函数分析:
一个节点,每下坠
一层,最多需要比较2
次关键字;
若堆高为h
,某个节点在i
层,这个节点最多向下调整h-i
层,对比关键词次数 ≤ 2(h-i)
;
第i
层最多有$2^{i-1}$个节点,第i
层的节点总共需要对比关键词: $ <= 2^{i-1}*2(h-i) = 2^{i}(h-i) 次 $
只有1~(h-1)
层的节点才有可能需要进行下坠
所以,将整棵树调整为大顶堆,关键字对比次数: $ <= \sum_{i=h-1}^{1}2^{i}(h-i)次 $
n
个节点的完全二叉树树高 h
= $\lceil log_2n \rceil +1$
所以带入上式,令j=h-i
,关键字对比次数:
$ <= \sum_{j=1}^{h-1}2^{h-j}j <=2n\sum_{j=1}^{h-1}j/2^j (等比求和) <= 4n 次 $
heapify(int &arr, int n, int i)
函数分析:
稳定性:
7、归并排序(Merge Sort) 归并排序(MERGE-SORT)是利用归并 的思想实现的排序方法,该算法采用经典的分治 (divide-and-conquer)策略(分治法将问题分 (divide)成一些小的问题然后递归求解,而**治(conquer)**的阶段则将分的阶段得到的各答案”修补”在一起,即分而治之)。
7.1 算法描述
把长度为 n 的输入序列分成两个长度为 n/2 的子序列;
对这两个子序列分别采用归并排序;
将两个排序好的子序列合并成一个最终的排序序列
7.2 动图演示
7.3 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include "MergeSort.h" void MergeSort::sort (std::vector<int >& arr) { if (arr.size ()<=1 ) return ; std::vector<int > temp (arr.size()) ; mergeSort (arr,temp,0 ,arr.size ()-1 ); }void MergeSort::mergeSort (std::vector<int >& arr, std::vector<int >& temp, int left, int right) { if (left<right){ int mid = left + (right-left)/2 ; mergeSort (arr,temp,left,mid); mergeSort (arr,temp,mid+1 ,right); merge (arr,temp,left,mid,right); } }void MergeSort::merge (std::vector<int >& arr, std::vector<int >& temp, int left, int mid, int right) { int k=left,i=left,j=mid+1 ; while (i<=mid&&j<=right){ if (arr[i]<=arr[j]) temp[k++]=arr[i++]; else temp[k++]=arr[j++]; } while (i<=mid) temp[k++]=arr[i++]; while (j<=right) temp[k++]=arr[j++]; for (int f=left;f<=right;f++) arr[f] = temp[f]; }std::string MergeSort::name () const { return "MergeSort" ; }
7.4 算法分析 二路归并排序 从形态上来看就是一颗倒立的二叉树
满足 $ 归并趟数(除开初始序列) = h-1 = \lceil log_2n \rceil $
每趟归并时间复杂度为$O(n)$,算法时间复杂度为$O(nlog_2n)$
空间复杂度为$O(n)$,来自辅助数组B
算法是稳定的,你可以决定在merge()
的时候,相同的数那个应该在前
8、计数排序(Counting Sort) 计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
8.1 算法描述
找出待排序的数组中最大和最小的元素;
统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i 项;
对所有的计数累加(从 C 中的第一个元素开始,每一项和前一项相加);
反向填充目标数组:将每个元素 i 放在新数组的第 C(i)项,每放一个元素就将 C(i)减去 1。
8.2 动图演示
8.3 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include "CountingSort.h" #include <vector> #include <algorithm> void CountingSort::sort (std::vector<int >& arr) { int maxElement = *std::max_element (arr.begin (), arr.end ()); int minElement = *std::min_element (arr.begin (), arr.end ()); std::vector<int >count (maxElement-minElement+1 ,0 ); for (int num:arr){ count[num-minElement]++; } int index = 0 ; for (int i=0 ;i<count.size ();i++){ while (count[i]-- >0 ){ arr[index++] = i+minElement; } } }std::string CountingSort::name () const { return "CountingSort" ; }
8.4 算法分析 计数排序是一个稳定的排序算法。当输入的元素是 n 个 0 到 k 之间的整数时,时间复杂度是 O(n+k),空间复杂度也是 O(n+k),其排序速度快于任何比较排序算法。当 k 不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
9、桶排序(Bucket Sort) 桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
9.1 算法描述
设置一个定量的数组当作空桶;
遍历输入数据,并且把数据一个一个放到对应的桶里去;
对每个不是空的桶进行排序;
从不是空的桶里把排好序的数据拼接起来。
9.2 图片演示
9.3 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void BucketSort::sort (std::vector<int >& arr) { if (arr.empty ()) return ; int minElement = *std::min_element (arr.begin (), arr.end ()); int maxElement = *std::max_element (arr.begin (), arr.end ()); int bucketCount = (maxElement - minElement) / arr.size () + 1 ; std::vector<std::vector<int >> buckets (bucketCount); for (int num : arr) { int index = (num - minElement) * (bucketCount - 1 ) / (maxElement - minElement); buckets[index].push_back (num); } for (auto & bucket : buckets) { std::sort (bucket.begin (), bucket.end ()); } int index = 0 ; for (auto & bucket : buckets) { for (int num : bucket) { arr[index++] = num; } } }
9.4 算法分析 桶排序最好情况下使用线性时间 O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为 O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
10、基数排序(Radix Sort) 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
10.1 算法描述
取得数组中的最大数,并取得位数;
arr 为原始数组,从最低位开始取每个位组成 radix 数组;
对 radix 进行计数排序(利用计数排序适用于小范围数的特点);
10.2 动图演示
10.3 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 void RadixSort::sort (std::vector<int >& arr) { int max = getMax (arr); for (int exp = 1 ; max / exp > 0 ; exp *= 10 ) { countingSort (arr, exp); } }std::string RadixSort::name () const { return "RadixSort" ; }void RadixSort::countingSort (std::vector<int >& arr, int exp) { int n = arr.size (); std::vector<int > output (n) ; std::vector<int > count (10 , 0 ) ; for (int i = 0 ; i < n; i++) { count[(arr[i] / exp) % 10 ]++; } for (int i = 1 ; i < 10 ; i++) { count[i] += count[i - 1 ]; } for (int i = n - 1 ; i >= 0 ; i--) { int digit = (arr[i] / exp) % 10 ; output[count[digit] - 1 ] = arr[i]; count[digit]--; } for (int i = 0 ; i < n; i++) { arr[i] = output[i]; } }int RadixSort::getMax (const std::vector<int >& arr) { int max = arr[0 ]; for (int num : arr) { if (num > max) { max = num; } } return max; }
10.4 算法分析