目录html
在网上有好多大神的,那我还要不要写一遍。哎,大神的是大神的,和你飞某人有神魔关系,本身写一遍是为了本身理解,就当作是学习笔记吧。java
参考连接:ios
https://www.runoob.com/w3cnote/ten-sorting-algorithm.html算法
https://zhuanlan.zhihu.com/p/42586566shell
https://www.cnblogs.com/guoyaohua/p/8600214.htmlapi
时间复杂度数组
平方阶 (\(O(n^2)\)) 各种简单排序:直接插入、直接选择和冒泡排序。数据结构
线性对数阶 (\(O(nlog_2n)\)) 快速排序、堆排序和归并排序;框架
\(O(n+§)\),§ 是介于 0 和 1 之间的常数。 希尔排序dom
线性阶 (\(O(n)\)) 基数排序,此外还有桶、箱排序。
稳定性
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
稳定:若是a本来在b前面,而且a=b,排序以后a仍然在b前面;
不稳定:若是a本来在b前面,而且a=b,排序以后a可在b后面;
内排序:全部排序操做都在内存中完成;
外排序:因为数据太大,把数据放在磁盘中,而排序经过磁盘和内存的数据传输才能进行;
时间复杂度:一个算法执行所耗费的时间;
空间复杂度:运行完一个程序所消耗的内存大小
常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每一个数都必须和其余数进行比较,才能肯定本身的位置。
在冒泡排序之类的排序中,问题规模为n,又由于须要比较n次,因此平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模经过分治法消减为logN次,因此时间复杂度平均O(nlogn)。
比较排序的优点是,适用于各类规模的数据,也不在意数据的分布,都能进行排序。能够说,比较排序适用于一切须要排序的状况。
计数排序、基数排序、桶排序则属于非比较排序。非比较排序是经过肯定每一个元素以前,应该有多少个元素来排序。针对数组arr,计算arr[i]以前有多少个元素,则惟一肯定了arr[i]在排序后数组中的位置。
非比较排序只要肯定每一个元素以前的已有的元素个数便可,全部一次遍历便可解决。算法时间复杂度O(n)。
非比较排序时间复杂度底,但因为非比较排序须要占用空间来肯定惟一位置。因此对数据规模和数据分布有必定的要求。
直接插入排序(Insertion Sort)简单直观,基本思想是将待排记录按关键字插入到前面已经排好的序列中,对于未排数据,从已经排列的数据从后往前扫描,找到相应的位置插入。因此在从后往前扫描时,须要反复将已经排序的元素逐步后挪。
通常来讲,插入排序都采用in-place在数组上实现。具体算法描述以下:
void insertion_sort(int arr[],int len){ for(int i=1;i<len;i++){ int key=arr[i]; int j=i-1; while((j>=0) && (key<arr[j])){ arr[j+1]=arr[j]; j--; } arr[j+1]=key; } }
**空间上**:只需一个记录的辅助空间
时间上:排序的基本操做为比较和移动
最佳状况 当待排列的记录按关键字非递减有序排列(正序)时,所需进行关键字之间的比较次数最小为n-1 (\(\sum_{i=2}^n1\)), 记录不须要移动, \(O(n)\)
最差状况 当当待排列的记录按关键字非递增有序排列(逆序)时,所需进行关键字之间的比较次数最大(n+2)(n-1)/2(\(\sum_{i=2}^ni\) ), 记录移动次数也变为最大(n+4)(n-1)/2(\(\sum_{i=2}^n{i+1}\))
平均状况 可取上述最大值和最小值的平均值,\(O(n^2)\)
因为只须要找到不大于当前数的位置而并不须要交换,所以,直接插入排序是稳定的排序方法
插入排序因为 \(O(n^2)\)的复杂度,在数组较大的时候不适用。可是,在数据比较少的时候,是一个不错的选择,通常作为快速排序的扩充。例如,在STL的sort算法和stdlib的qsort算法中,都将插入排序做为快速排序的补充,用于少许元素的排序。又如,在JDK 7 java.util.Arrays所用的sort方法的实现中,当待排数组长度小于47时,会使用插入排序。
因为直接插入排序的基本操做是查找和插入。能够用”折半查找“来实现,所以折半插入排序仅减小了关键字之间的比较次数,而记录的移动次数不变,时间复杂度仍为\(O(n)\)
void BInsertSort(int L[],int length){ //对顺序表L进行折半插入排序 int low,high,temp,mid; for( int i=1;i<=length;++i){ temp=L[i]; //将L.r[i ]暂存到temp low=0; high=i-1; while(low<=high){ //在r[low...high]中折半查找有序插入的位置 mid=(low+high)/2; //折半 if(temp<L[mid]) high=mid-1; //插入点在低半段 else low=mid+1; //插入点在高半区 } for( int j=i-1;j>=high+1;--j) L[j+1]=L[j];//记录后移 L[high+1]=temp;//插入 } }
在希尔排序出现以前,计算机界广泛存在“排序算法不可能突破\(O(n^2)\)”的观点。希尔排序是第一个突破\(O(n^2)\)的排序算法,它是简单插入排序的改进版。希尔排序的提出,主要基于如下两点:
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
希尔增量 n/2 (n/2)/2 ... 1
希尔增量 n/2 (n/2)/2 ... 1 void shell_sort(int arr[], int len) { int gap, i, j; int temp; for (gap = len >> 1; gap > 0; gap >>= 1) for (i = gap; i < len; i++) { temp = arr[i]; for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) arr[j + gap] = arr[j]; arr[j + gap] = temp; } }
\(O(n^{3/2})\) by Knuth 1, 4, 13, 40, 121...
template<typename T> void shell_sort(T array[], int length) { int h = 1; while (h < length / 3) { h = 3 * h + 1; } while (h >= 1) { for (int i = h; i < length; i++) { for (int j = i; j >= h && array[j] < array[j - h]; j -= h) { std::swap(array[j], array[j - h]); } } h = h / 3; } }
希尔排序的增量数列能够任取,须要的惟一条件是最后一个必定为1(由于要保证按1有序)。可是,不一样的数列选取会对算法的性能形成极大的影响。上面的代码演示了两种增量。
切记:增量序列中每两个元素最好不要出现1之外的公因子!(很显然,按4有序的数列再去按2排序意义并不大)。
下面是一些常见的增量序列。
Donald Shell增量。折半下降直到1。据研究,使用希尔增量,其时间复杂度仍是\(O(n^2)\)。
Hibbard 增量。
递推公式:
\(h_1=1,h_i=2∗h_{i−1}+1\)
1,3,7,15,31,63,127,255,511,1023......
最坏时间复杂度为$ Θ(N^{3/2})$;平均时间复杂度约为 \(O(N^{5/4})\)。
Knuth增量。
递推公式:
\(h_1=1,h_i=3∗h_{i−1}+1\)
1,4,13,40,121,364,1093,3280......
最坏时间复杂度为 \(O(N^{4/3})O(N^{4/3})\);平均时间复杂度约为 \(O(N^{7/6})\)
Sedgewick增量。
通项公式:
\(h_i=max(9∗4^j−9∗2^j+1,4^k−3∗2^k+1)\)
1,5,19,41,109.209,505,929,2161,3905......
空间复杂度:只需一个记录的辅助空间
咱们都知道插入排序是稳定算法。可是,Shell排序是一个屡次插入的过程。在一次插入中咱们能确保不移动相同元素的顺序,但在屡次的插入中,相同元素彻底有可能在不一样的插入轮次被移动,最后稳定性被破坏,所以,Shell排序不是一个稳定的算法
Shell排序虽然快,可是毕竟是插入排序,其数量级并无后起之秀--快速排序\(O(n log n)\) 。在大量数据面前,Shell排序不是一个好的算法。可是,中小型规模的数据彻底可使用它。
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,若是它们的顺序错误就把它们交换过来。每趟排序使得关键字大的记录安置到最后。在冒泡过程当中,关键字较小的记录比如水中的气泡逐趟上升,而关键字较大的记录数则比如石块逐趟下沉,每一趟有一块“最大”的石块下沉到底部。
首先将第一个记录的关键字和第二个记录的关键字进行比较,若是未逆序则进行交换,以此类推,一直到第n-1和n个记录的关键字进行比较为止。上述过程称为一次冒泡排序,其结果使得关键字最大的记录被安排在最后一个记录的位置上。
而后进行第二趟排序,对前n-1个记录进行一样操做,其结果是关键字次大的记录数被安排在第n-1个位置。
通常的,第i趟排序是从\(L.r[1]\)到\(L.r[n-i+1]\)依次比较相邻两个记录的关键字,并在逆序时交换位置,其结果是这\(n-i+1\)个记录中关键字最大的记录被交换到第\(n-i+1\)的位置。
整个排序过程须要进行\(k(1\leq k<n)\)趟排序。显然,判断排序结束的条件是“在一条趟排序过程当中没有进行交换记录的操做”。
#include <iostream> using namespace std; template<typename T> //整数或浮点数皆可以使用,若要使用类(class)或结构体(struct)时必须重载大于(>)运算符 void bubble_sort(T arr[], int len) { int i, j; for (i = 0; i < len - 1; i++) for (j = 0; j < len - 1 - i; j++) if (arr[j] > arr[j + 1]) swap(arr[j], arr[j + 1]); } int main() { int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 }; int len = (int) sizeof(arr) / sizeof(*arr); bubble_sort(arr, len); for (int i = 0; i < len; i++) cout << arr[i] << ' '; cout << endl; float arrf[] = { 17.5, 19.1, 0.6, 1.9, 10.5, 12.4, 3.8, 19.7, 1.5, 25.4, 28.6, 4.4, 23.8, 5.4 }; len = (float) sizeof(arrf) / sizeof(*arrf); bubble_sort(arrf, len); for (int i = 0; i < len; i++) cout << arrf[i] << ' '<<endl; return 0; }
在数据彻底有序的时候展示出最优时间复杂度,为\(O(n)\)。其余状况下,几乎老是\(O( n^2 )\)。所以,算法在数据基本有序的状况下,性能最好
在相邻元素相等时,它们并不会交换位置,因此,冒泡排序是稳定排序。
要使算法在最佳状况下有\(O(n)\)复杂度,须要作一些改进,增长一个swap
的标志,当前一轮没有进行交换时,说明数组已经有序,没有必要再进行下一轮的循环了,直接退出。
public static void bubbleSort(int[] arr) { int temp = 0; boolean swap; for (int i = arr.length - 1; i > 0; i--) { // 每次须要排序的长度 swap=false; for (int j = 0; j < i; j++) { // 从第一个元素到第i个元素 if (arr[j] > arr[j + 1]) { temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; swap=true; } }//loop j if (swap==false){ break; } }//loop i }// method bubbleSort
快速排序对于大数据的优秀排序性能和相同复杂度算法中相对简单的实现使它注定获得比其余算法更多的宠爱。
快速排序的最坏运行状况是 O(n²),好比说顺序数列的快排。但它的平摊指望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小不少。因此,对绝大多数顺序性较弱的随机数列而言,快速排序老是优于归并排序
基本思想是,经过一趟排序将待排记录分割为独立的两部分,其中一部分的记录的关键字均小于另外一部分记录的关键字,则可分别对这两部分记录继续排序,以达到整个记录均有序。
//严蔚敏《数据结构》标准分割函数 Paritition1(int A[], int low, int high) { int pivot = A[low]; //枢纽记录关键字 while (low < high) { //从表的两端交替向中间扫描 while (low < high && A[high] >= pivot) { --high; } A[low] = A[high]; //将枢纽记录小的记录移到低端 while (low < high && A[low] <= pivot) { ++low; } A[high] = A[low]; //将枢纽记录大的记录移到高端 } A[low] = pivot; //枢纽记录到位 return low; //返回枢纽位置 } void QuickSort(int A[], int low, int high) //快排母函数 { if (low < high) { //长度大于1 int pivot = Paritition1(A, low, high); //将A一分为二 QuickSort(A, low, pivot - 1); //对低子表递归排序 QuickSort(A, pivot + 1, high); } }
空间上
快速排序须要一个栈空间来实现递归。若每趟排序都将记录均匀分割为长度近似的两个子序列,则栈的最大深度为\(\lfloor log_2 n\rfloor +1\) (包括最外层参量进栈),可是,若每趟排序以后,枢纽位置均偏向子序列的一端,则为最坏状况,栈的最大深度为\(n\)。
时间上 \(O(nlogn)\)
一般,快速排序被认为是,在全部同数量级(\(O(nlogn)\))的排序方法中,其平均性能最好。可是若是初始记录按关键字有序或者基本有序时,快速排序将蜕化为冒泡排序,其时间复杂度为\(O(n^2)\)。为改进之,一般依“三者取中”的法则来选取枢纽记录.经验证实,采用三者取中的规则可大大改善快速排序在最坏状况下的性能。然而,即便如此,也不能使快速排序在待排记录序列按关键字有序时达到\(O(n)\)的时间复杂度。
快速排序并非稳定的。这是由于咱们没法保证相等的数据按顺序被扫描到和按顺序存放。
STL库将快速排序作了改进。没有使用两边递归,而是一边递归,另外一边循环。而且使用了“三者取中”的规则。
template<typename _RandomAccessIterator, typename _Size, typename _Compare> void __introsort_loop(_RandomAccessIterator __first, _RandomAccessIterator __last, _Size __depth_limit, _Compare __comp) { while (__last - __first > int(_S_threshold)) { /*堆排序部分,当快速排序层数过大时,改用堆排序 if (__depth_limit == 0) { std::__partial_sort(__first, __last, __last, __comp); return; } --__depth_limit; */ //将集合分为两部分 _RandomAccessIterator __cut = std::__unguarded_partition_pivot(__first, __last, __comp); //后一半递归 std::__introsort_loop(__cut, __last, __depth_limit, __comp); //前一半继续排序 __last = __cut; } } // template<typename _RandomAccessIterator, typename _Compare> inline _RandomAccessIterator __unguarded_partition_pivot(_RandomAccessIterator __first, _RandomAccessIterator __last, _Compare __comp) { //取三点中值 _RandomAccessIterator __mid = __first + (__last - __first) / 2; std::__move_median_to_first(__first, __first + 1, __mid, __last - 1, __comp); return std::__unguarded_partition(__first + 1, __last, __first, __comp); } //快速排序的主体部分 template<typename _RandomAccessIterator, typename _Compare> _RandomAccessIterator __unguarded_partition(_RandomAccessIterator __first, _RandomAccessIterator __last, _RandomAccessIterator __pivot, _Compare __comp) { while (true) { while (__comp(__first, __pivot)) ++__first; --__last; while (__comp(__pivot, __last)) --__last; if (!(__first < __last)) return __first; std::iter_swap(__first, __last); ++__first; } }
冒泡算法,每次比较若是发现较小的元素在后面,就交换两个相邻的元素。而选择排序算法的改进在于:先并不急于调换位置,先从A[1]开始逐个检查,看哪一个数最小就记下该数所在的位置P,等一躺扫描完毕,再把A[P]和A[1]对调,这时A[1]到A[10]中最小的数据就换到了最前面的位置。因此,选择排序每扫描一遍数组,只须要一次真正的交换,而冒泡可能须要屡次。比较的次数是同样的。
template<typename T> //整數或浮點數皆可以使用,若要使用物件(class)時必須設定大於(>)的運算子功能 void selection_sort(std::vector<T>& arr) { for (int i = 0; i < arr.size() - 1; i++) { int min = i; for (int j = i + 1; j < arr.size(); j++) if (arr[j] < arr[min]) min = j; std::swap(arr[i], arr[min]); } }
用数组实现的选择排序是不稳定的,用链表实现的选择排序是稳定的。
不过,通常提到排序算法时,你们每每会默认是数组实现,因此选择排序是不稳定的
所须要的进行记录移动的操做次数最少为“0”,最大为\(3(n-1)\),然而,不论记录的初始排列如何,所须要的进行关键字间的比较次数相同均为\(n(n-1)/2\)。所以总的时间复杂度为\(O(n^2)\)
选择排序实现也比较简单,而且因为在各类状况下复杂度波动小,所以通常是优于冒泡排序的。在全部的彻底交换排序中,选择排序也是比较不错的一种算法。可是,因为固有的\(O(n^2)\)复杂度,选择排序在海量数据面前显得力不从心。所以,它适用于简单数据排序。
堆排序(Heap Sort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。能够利用数组的特色快速定位指定索引的元素。堆排序就是把最大堆堆顶的最大数取出,将剩余的堆继续调整为最大堆,再次将堆顶的最大数取出,这个过程持续到剩余数只有一个时结束。
堆是一种特殊的彻底二叉树(complete binary tree)。彻底二叉树的一个“优秀”的性质是,除了最底层以外,每一层都是满的,这使得堆能够利用数组来表示(普通的通常的二叉树一般用链表做为基本容器表示),每个结点对应数组中的一个元素。
以下图,是一个堆和数组的相互关系:
对于给定的某个结点的下标 i,能够很容易的计算出这个结点的父结点、孩子结点的下标:
二叉堆通常分为两种:最大堆和最小堆。
最大堆:
最大堆中的最大元素值出如今根结点(堆顶)
堆中每一个父节点的元素值都大于等于其孩子结点(若是存在)
最小堆:
最小堆中的最小元素值出如今根结点(堆顶)
堆中每一个父节点的元素值都小于等于其孩子结点(若是存在)
堆排序就是把最大堆堆顶的最大数取出,将剩余的堆继续调整为最大堆,再次将堆顶的最大数取出,这个过程持续到剩余数只有一个时结束。在堆中定义如下几种操做:
相应的,几个计算公式也要做出相应调整:
堆能够支持多种操做,但如今咱们关心的只有两个问题:
先看第二个问题。假定咱们已经有一个现成的大根堆。如今咱们删除了根元素,但并无移动别的元素。想一想发生了什么:根元素空了,但其它元素还保持着堆的性质。咱们能够把最后一个元素(代号A)移动到根元素的位置。若是不是特殊状况,则堆的性质被破坏。但这仅仅是因为A小于其某个子元素。因而,咱们能够把A和这个子元素调换位置。若是A大于其全部子元素,则堆调整好了;不然,重复上述过程,A元素在树形结构中不断“下沉”,直到合适的位置,数组从新恢复堆的性质。上述过程通常称为“筛选”,方向显然是自上而下。
删除后的调整,是把最后一个元素放到堆顶,自上而下比较
删除一个元素是如此,插入一个新元素也是如此。不一样的是,咱们把新元素放在末尾,而后和其父节点作比较,即自下而上筛选。
插入是把新元素放在末尾,自下而上比较
那么,第一个问题怎么解决呢?
常规方法是从第一个非叶子结点向下筛选,直到根元素筛选完毕。这个方法叫“筛选法”,须要循环筛选n/2个元素。
但咱们还能够借鉴“插入排序”的思路。咱们能够视第一个元素为一个堆,而后不断向其中添加新元素。这个方法叫作“插入法”,须要循环插入(n-1)个元素。
因为筛选法和插入法的方式不一样,因此,相同的数据,它们创建的堆通常不一样。大体了解堆以后,堆排序就是水到渠成的事情了。
创建最大堆,倒着输出,在最后一个位置输出最大值,次末位置输出次大值……因为每次输出的最大元素会腾出第一个空间,所以,咱们刚好能够放置这样的元素而不须要额外空间。
#include <iostream> #include <algorithm> using namespace std; void max_heapify(int arr[], int start, int end) { // 创建父節點指標和子節點指標 int dad = start; int son = dad * 2 + 1; while (son <= end) { // 若子節點指標在範圍內才作比較 if (son + 1 <= end && arr[son] < arr[son + 1]) // 先比較兩個子節點大小,選擇最大的 son++; if (arr[dad] > arr[son]) // 若是父節點大於子節點表明調整完畢,直接跳出函數 return; else { // 否則交換父子內容再繼續子節點和孫節點比較 swap(arr[dad], arr[son]); dad = son; son = dad * 2 + 1; } } } void heap_sort(int arr[], int len) { // 初始化,i從最後一個父節點開始調整 for (int i = len / 2 - 1; i >= 0; i--) max_heapify(arr, i, len - 1); // 先將第一個元素和已经排好的元素前一位作交換,再從新調整(刚调整的元素以前的元素),直到排序完畢 for (int i = len - 1; i > 0; i--) { swap(arr[0], arr[i]); max_heapify(arr, 0, i - 1); } } int main() { int arr[] = {60,91,13,19,30,10,35,81,77,20,22,96,31,46,65}; int len = (int) sizeof(arr) / sizeof(*arr); heap_sort(arr, len); for (int i = 0; i < len; i++) cout << arr[i] << ' '; cout << endl; return 0; }
堆排序存在大量的筛选和移动过程,属于不稳定的排序算法。
空间上
仅需一个记录大小供交换用的辅助空间
时间上 \(O(nlogn)\)
堆排序在最坏状况下,其时间复杂度也为\(O(nlogn)\)。
其运行时间主要耗费在建最初堆和调整新堆时进行的反复“筛选”上。对深度为\(k\)的堆,筛选算法中进行关键字的比较次数至多为\(2(k-1)\)次,则在建含\(n\)个元素、深度为\(h\)的堆时,总共进行的关键字比较次数不会超过\(4n\)。又,\(n\)个结点的彻底二叉树的深度为\(\lfloor log_2 n \rfloor +1\),则进行重建新堆时调用Heap Adjust 过程\(n-1\)次,总共进行的比较次数不超过$2n(\lfloor log_2 n \rfloor) $。
堆排序在创建堆和调整堆的过程当中会产生比较大的开销,在元素少的时候并不适用。可是,在元素比较多的状况下,仍是不错的一个选择。尤为是在解决诸如“前n大的数”一类问题时,几乎是首选算法。
归并排序是创建在归并操做上的一种有效的排序算法。该算法是采用分治法的一个很是典型的应用。将已有序的子序列合并,获得彻底有序的序列;即先使每一个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
两种方法
原理以下(假设序列共有n个元素):
C++
递归版:
void MergeSort(vector<int> &Array, int front, int end) { if (front >= end) return; int mid = (front + end) / 2; //将Array平分 MergeSort(Array, front, mid);//递归地将Array[front,mid]归并为有序 MergeSort(Array, mid + 1, end);//递归地将Array[mid+1,end]归并为有序 Merge(Array, front, mid, end);//将Array[front,mid],Array[mid+1,end]合并 } void Merge(vector<int> &Array, int front, int mid, int end) { // preconditions: // Array[front...mid] is sorted // Array[mid+1 ... end] is sorted // Copy Array[front ... mid] to LeftSubArray // Copy Array[mid+1 ... end] to RightSubArray vector<int> LeftSubArray(Array.begin() + front, Array.begin() + mid + 1); vector<int> RightSubArray(Array.begin() + mid + 1, Array.begin() + end + 1); int idxLeft = 0, idxRight = 0; LeftSubArray.insert(LeftSubArray.end(), numeric_limits<int>::max()); RightSubArray.insert(RightSubArray.end(), numeric_limits<int>::max()); // Pick min of LeftSubArray[idxLeft] and RightSubArray[idxRight], and put into Array[i] for (int i = front; i <= end; i++) { if (LeftSubArray[idxLeft] < RightSubArray[idxRight]) { Array[i] = LeftSubArray[idxLeft]; idxLeft++; } else { Array[i] = RightSubArray[idxRight]; idxRight++; } } }
迭代版:
template<typename T> // 整數或浮點數皆可以使用,若要使用物件(class)時必須設定"小於"(<)的運算子功能 void merge_sort(T arr[], int len) { T *a = arr; T *b = new T[len]; for (int seg = 1; seg < len; seg += seg) {//控制seg迭代来实现 for (int start = 0; start < len; start += seg + seg) { int low = start, mid = min(start + seg, len), high = min(start + seg + seg, len); int k = low; int start1 = low, end1 = mid; int start2 = mid, end2 = high; while (start1 < end1 && start2 < end2) b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++]; while (start1 < end1) b[k++] = a[start1++]; while (start2 < end2) b[k++] = a[start2++]; } T *temp = a; a = b; b = temp; } if (a != arr) { for (int i = 0; i < len; i++) b[i] = a[i]; b = a; } delete[] b; }
由于咱们在遇到相等的数据的时候必然是按顺序“抄写”到辅助数组上的,因此,归并排序一样是稳定算法
空间上
空间复杂度O(n),实现归并排序须要和记录等数量的辅助空间。
时间上
时间复杂度\(O(nlogn)\)
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不一样的数字,而后按每一个位数分别比较。因为整数也能够表达字符串(好比名字或日期)和特定格式的浮点数,因此基数排序也不是只能使用于整数。
int maxbit(int data[], int n) //辅助函数,求数据的最大位数 { int maxData = data[0]; ///< 最大数 /// 先求出最大数,再求其位数,这样有原先依次每一个数判断其位数,稍微优化点。 for (int i = 1; i < n; ++i) { if (maxData < data[i]) maxData = data[i]; } int d = 1; int p = 10; while (maxData >= p) { //p *= 10; // Maybe overflow maxData /= 10; ++d; } return d; /* int d = 1; //保存最大的位数 int p = 10; for(int i = 0; i < n; ++i) { while(data[i] >= p) { p *= 10; ++d; } } return d;*/ } void radixsort(int data[], int n) //基数排序 { int d = maxbit(data, n); int *tmp = new int[n]; int *count = new int[10]; //计数器 int i, j, k; int radix = 1; for(i = 1; i <= d; i++) //进行d次排序 { for(j = 0; j < 10; j++) count[j] = 0; //每次分配前清空计数器 for(j = 0; j < n; j++) { k = (data[j] / radix) % 10; //统计每一个桶中的记录数 count[k]++; } for(j = 1; j < 10; j++) count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每一个桶 for(j = n - 1; j >= 0; j--) //将全部桶中记录依次收集到tmp中 { k = (data[j] / radix) % 10; tmp[count[k] - 1] = data[j]; count[k]--; } for(j = 0; j < n; j++) //将临时数组的内容复制到data中 data[j] = tmp[j]; radix = radix * 10; } delete []tmp; delete []count; }
基数排序是按照低位先排序,而后收集;再按照高位排序,而后再收集;依次类推,直到最高位。先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,因此是稳定的。
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 做为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有肯定范围的整数。
计数排序的算法的原理,它不须要去跟其余元素比来比去,而是一开始就知道本身的位置,因此直接归位。在计数的该元素出现的词频数组里面,出现一次,就直接加一次便可,若是没有出现改位置就是0。最后该位置的词频,就是表明其在原始数组里面出现的次数。因为词频数组的index是从0开始,因此最后直接遍历输出这个数组里面的每个大于0的元素值便可。
public class CountingSort implements IArraySort { @Override public int[] sort(int[] sourceArray) throws Exception { // 对 arr 进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); int maxValue = getMaxValue(arr); return countingSort(arr, maxValue); } private int[] countingSort(int[] arr, int maxValue) { int bucketLen = maxValue + 1; int[] bucket = new int[bucketLen]; for (int value : arr) { bucket[value]++; } int sortedIndex = 0; for (int j = 0; j < bucketLen; j++) { while (bucket[j] > 0) { arr[sortedIndex++] = j; bucket[j]--; } } return arr; } private int getMaxValue(int[] arr) { int maxValue = arr[0]; for (int value : arr) { if (maxValue < value) { maxValue = value; } } return maxValue; } }
当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。因为用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,须要大量时间和内存。
空间上
\(O(n)\)
排序目标要可以映射到整数域,其最大值最小值应当容易辨别。另外,计数排序须要占用大量空间,它比较适用于数据比较集中的状况。
计数排序在特定的状况下,排序效率极高,可是若是排序的计数空间范围过大,而实际元素个数很是小的状况,效率就会很是差,好比,我只有3个元素,3,1,500000,这样的状况实际上是不适合用计数排序的
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的肯定。为了使桶排序更加高效,咱们须要作到这两点:
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响相当重要
public class BucketSort implements IArraySort { private static final InsertSort insertSort = new InsertSort(); @Override public int[] sort(int[] sourceArray) throws Exception { // 对 arr 进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); return bucketSort(arr, 5); } private int[] bucketSort(int[] arr, int bucketSize) throws Exception { if (arr.length == 0) { return arr; } int minValue = arr[0]; int maxValue = arr[0]; for (int value : arr) { if (value < minValue) { minValue = value; } else if (value > maxValue) { maxValue = value; } } int bucketCount = (int) Math.floor((maxValue - minValue) / bucketSize) + 1; int[][] buckets = new int[bucketCount][0]; // 利用映射函数将数据分配到各个桶中 for (int i = 0; i < arr.length; i++) { int index = (int) Math.floor((arr[i] - minValue) / bucketSize); buckets[index] = arrAppend(buckets[index], arr[i]); } int arrIndex = 0; for (int[] bucket : buckets) { if (bucket.length <= 0) { continue; } // 对每一个桶进行排序,这里使用了插入排序 bucket = insertSort.sort(bucket); for (int value : bucket) { arr[arrIndex++] = value; } } return arr; } /** * 自动扩容,并保存数据 * * @param arr * @param value */ private int[] arrAppend(int[] arr, int value) { arr = Arrays.copyOf(arr, arr.length + 1); arr[arr.length - 1] = value; return arr; } }
须要k个桶,而且这些桶中放了n个元素,空间复杂度为\(O(n+k)\)
显然,当输入的数据能够均匀的分配到每个桶中时,速度最快。当输入的数据被分配到了同一个桶中时,速度最慢。
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差别: