总结一下几点的常识:
- 查找和排序算法是算法的入门知识,其经典思想可以用于很多算法当中。也就是说,排序然后是查找是确定最优解空间的必然手段
- 如果一个排序算法对相同元素的位置关系不影响,则说明这个排序算法稳定。
- 全依赖“比较”操作的排序算法时间复杂度的一个下界O(N*logN)。
- 在查找算法中,基于比较的查找算法最好的时间复杂度也是O(logN)。比如折半查找、平衡二叉树、红黑树等。
- 分析一个算法,要从(1)算法原理,(2)算法过程,(3)算法实现,(4)算法复杂度,包括时间复杂度,空间复杂度,(5)经典改进(6)适用场景 等方面进行分析。
因为其实现代码较短,应用较常见。所以在面试中经常会问到排序算法及其相关的问题。但万变不离其宗,只要熟悉了思想,灵活运用也不是难事。一般在面试中最常考的是快速排序和归并排序,并且经常有面试官要求现场写出这两种排序的代码。对这两种排序的代码一定要信手拈来才行。还有插入排序、冒泡排序、堆排序、基数排序、桶排序等。
面试官对于这些排序可能会要求比较各自的优劣、各种算法的思想及其使用场景。还有要会分析算法的时间和空间复杂度。
通常查找和排序算法的考察是面试的开始,如果这些问题回答不好,估计面试官都没有继续面试下去的兴趣都没了。所以想开个好头就要把常见的排序算法思想及其特点要熟练掌握,有必要时要熟练写出代码。接下来我们就分析一下常见的排序算法及其使用场景。限于篇幅,某些算法的详细演示和图示请自行寻找详细的参考。
1. 快速排序(最常用,快排)
快速排序一听名字就觉得很高端,在实际应用当中快速排序确实也是表现最好的排序算法。快速排序虽然高端,但其实其思想是来自冒泡排序,冒泡排序是通过相邻元素的比较和交换把最小的冒泡到最顶端,而快速排序是比较和交换小数和大数,这样一来不仅把小数冒泡到上面同时也把大数沉到下面。
举个栗子:对5,3,8,6,4这个无序序列进行快速排序,思路是右指针找比基准数小的,左指针找比基准数大的,交换之(递增排序)。
5,3,8,6,4 用5作为比较的基准,最终会把5小的移动到5的左边,比5大的移动到5的右边。
5,3,8i,6,4j 首先设置i,j两个指针分别指向两端,j指针先扫描(思考一下为什么?)4比5小停止。然后i扫描,8比5大停止。交换i,j位置。
5,3,4ij,6,8 然后j指针再先扫描,这时j扫描4时两指针相遇。停止。然后交换4和基准数。
4,3,5,6,8 一次划分后达到了左边比5小,右边比5大的目的。之后对左右子序列递归排序(递归调用上述流程),最终得到有序序列。
上面留下来了一个问题为什么一定要j指针先动呢?首先这也不是绝对的,这取决于基准数的位置,因为在最后两个指针相遇的时候,要交换基准数到相遇的位置。一般选取第一个数作为基准数,那么就是在左边,所以最后相遇的数要和基准数交换,那么相遇的数一定要比基准数小。所以j指针先移动才能先找到比基准数小的数。
快速排序是不稳定的,其时间平均时间复杂度是O(nlgn),空间复杂度为O(1)。
实现代码:
1 public class QuickSort { 2 //一次划分,递增排序, 返回本次划分位置 3 public static int partition(int[] arr, int left, int right) { 4 int pivotKey = arr[left]; 5 int pivotPointer = left; 6 //终止内循环条件 7 while(left < right) { 8 while(right > pivotKey)//保证最终左边不大于 A 9 right --; 10 while(left < pivotKey) 11 left ++; 12 swap(arr, left, right); //把大的交换到右边,把小的交换到左边。 13 } 14 swap(arr, pivotPointer, left); //最后把pivot交换到中间 15 return left; 16 } 17 18 public static void quickSort(int[] arr, int left, int right) { 19 if(left >= right) //递归终止条件 20 return ; 21 int pivotPos = partition(arr, left, right); 22 quickSort(arr, left, pivotPos-1); 23 quickSort(arr, pivotPos+1, right); 24 } 25 //入口 26 public static void sort(int[] arr) { 27 if(arr == null || arr.length == 0) 28 return ; 29 quickSort(arr, 0, arr.length-1); 30 } 31 32 public static void swap(int[] arr, int left, int right) { 33 int temp = arr[left]; 34 arr[left] = arr[right]; 35 arr[right] = temp; 36 } 37 38 }
其实上面的代码还可以再优化,上面代码中基准数已经在pivotKey中保存了,所以不需要每次交换都设置一个temp变量,在交换左右指针的时候只需要先后覆盖就可以了。这样既能减少空间的使用还能降低赋值运算的次数。优化代码如下:
1 public class QuickSort { 2 3 /** 4 * 划分 5 * @param arr 6 * @param left 7 * @param right 8 * @return 9 */ 10 public static int partition(int[] arr, int left, int right) { 11 int pivotKey = arr[left]; 12 13 while(left < right) { 14 while(right > pivotKey) 15 right --; 16 arr[left] = arr[right]; //把小的移动到左边,找到右边“不大于”元素,直接覆盖“基准内” 17 while(left < pivotKey) 18 left ++; 19 arr[right] = arr[left]; //把大的移动到右边, 找到左边“不小于”元素,直接“覆盖” 20 } 21 arr[left] = pivotKey; //最后把pivot赋值到中间 22 return left; 23 } 24 25 /** 26 * 递归划分子序列 27 * @param arr 28 * @param left 29 * @param right 30 */ 31 public static void quickSort(int[] arr, int left, int right) { 32 if(left >= right) 33 return ; 34 int pivotPos = partition(arr, left, right); 35 quickSort(arr, left, pivotPos-1); 36 quickSort(arr, pivotPos+1, right); 37 } 38 39 public static void sort(int[] arr) { 40 if(arr == null || arr.length == 0) 41 return ; 42 quickSort(arr, 0, arr.length-1); 43 } 44 45 }
总结快速排序的思想:冒泡+二分+递归分治,慢慢体会。。。
2. 归并排序
归并排序是另一种不同的排序方法,因为归并排序使用了递归分治的思想,所以理解起来比较容易。其基本思想是,先递归划分子问题,然后合并结果。把待排序列看成由两个有序的子序列,然后合并两个子序列,然后把子序列看成由两个有序序列。。。。。倒着来看,其实就是先两两合并,然后四四合并。。。最终形成有序序列。空间复杂度为O(n),时间复杂度为O(nlogn)。
举个栗子:
实现代码:
1 public class MergeSort { 2 //入口 3 public static void mergeSort(int[] arr) { 4 mSort(arr, 0, arr.length-1); 5 } 6 7 /** 8 * 递归分治 9 * @param arr 待排数组 10 * @param left 左指针 11 * @param right 右指针 12 */ 13 public static void mSort(int[] arr, int left, int right) { 14 if(left >= right) 15 return ; 16 int mid = (left + right) / 2; 17 18 mSort(arr, left, mid); //递归排序左边 19 mSort(arr, mid+1, right); //递归排序右边 20 merge(arr, left, mid, right); //合并 21 } 22 23 /** 24 * 合并两个有序数组 25 * @param arr 待合并数组 26 * @param left 左指针 27 * @param mid 中间指针 28 * @param right 右指针 29 */ 30 public static void merge(int[] arr, int left, int mid, int right) { 31 //[left, mid] [mid+1, right] 32 int[] temp = new int[right - left + 1]; //中间数组(空间复杂度消耗) 33 34 int i = left; 35 int j = mid + 1; 36 int k = 0; 37 while(i <= right) { 38 if(arr[i] <= arr[j]) { 39 temp[k++] = arr[i++]; 40 } 41 else { 42 temp[k++] = arr[j++]; 43 } 44 } 45 46 while(i <= mid) { 47 temp[k++] = arr[i++]; 48 } 49 50 while(j <= right) { 51 temp[k++] = arr[j++]; 52 } 53 54 for(int p=0; p<= right - left; p++) { 55 arr[left + p] = temp[p]; 56 } 57 58 } 59 }
3. 冒泡排序
冒泡排序是最简单的排序之一了,其大体思想就是通过与相邻元素的比较和交换来把小的数交换到最前面。这个过程类似于水泡向上升一样,因此而得名。
举个栗子
对5,3,8,6,4这个无序序列进行冒泡排序
首先从后向前冒泡,4和6比较,把4交换到前面,序列变成5,3,8,4,6。
同理4和8交换,变成5,3,4,8,6,3和4无需交换。5和3交换,变成3,5,4,8,6,3.这样一次冒泡就完了,把最小的数3排到最前面了。对剩下的序列依次冒泡就会得到一个有序序列。
冒泡排序的时间复杂度为O(n^2),空间复杂度O(1)。
n 个元素需要比较 N-1此循环, 每次比较 N-i
递增排序:
1 public class BubbleSort { 2 3 public static void bubbleSort(int[] arr) { 4 if(arr == null || arr.length == 0) 5 return ; 6 for(int i=0; i< arr.length-1; i++) { 7 for(int j=arr.length-1; j>i; j--) { 8 if(arr[j]< arr[j-1]) { 9 swap(arr, j-1, j); 10 } 11 } 12 } 13 } 14 15 16 public static void swap(int[] arr, int i, int j) { 17 int temp = arr[i]; 18 arr[i] = arr[j]; 19 arr[j] = temp; 20 } 21 }
4. 选择排序
选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择。
举个栗子,对5,3,8,6,4这个无序序列进行简单选择排序,首先要选择5以外的最小数来和5交换,也就是选择3和5交换,一次排序后就变成了3,5,8,6,4.对剩下的序列一次进行选择和交换,最终就会得到一个有序序列。其实选择排序可以看成冒泡排序的优化,因为其目的相同,只是选择排序只有在确定了最小数的前提下才进行交换,大大减少了交换的次数。
选择排序的时间复杂度为O(n^2), 空间复杂度O(1)。
n个元素,只需要比较 N-1 此循环,内部每i 次需要比较 N-1-i个循环
1 public class SelectSort { 2 3 public static void selectSort(int[] arr) { 4 if(arr == null || arr.length == 0) 5 return ; 6 int minIndex = 0; 7 for(int i=0; i < arr.length -1; i++)//只需要比较n-1次 8 minIndex = i; 9 for(int j=i+1; j<= arr.length -1; j++)//从i+1开始比较,因为minIndex默认为i了,i就没必要比了。 10 if(arr[j] < arr[minIndex]) { 11 minIndex = j; 12 } 13 } 14 15 if(minIndex != i) { //如果minIndex不为i,说明找到了更小的值,交换之。 16 swap(arr, i, minIndex); 17 } 18 } 19 20 } 21 22 public static void swap(int[] arr, int i, int j) { 23 int temp = arr[i]; 24 arr[i] = arr[j]; 25 arr[j] = temp; 26 } 27 28 }
5. 插入排序
<通过平移后续的元素,确保前面的顺序>
插入排序不是通过交换位置而是通过比较找到合适的位置插入元素来达到排序的目的的。
相信大家都有过打扑克牌的经历,特别是牌数较大的。在分牌时可能要整理自己的牌,牌多的时候怎么整理呢?就是拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是一样的。
举个栗子,对5,3,8,6,4这个无序序列进行简单插入排序,首先假设第一个数的位置时正确的,想一下在拿到第一张牌的时候,没必要整理。然后3要插到5前面,把5后移一位,变成3,5,8,6,4.想一下整理牌的时候应该也是这样吧。然后8不用动,6插在8前面,8后移一位,4插在5前面,从5开始都向后移一位。注意在插入一个数的时候要保证这个数前面的数已经有序。
简单插入排序的时间复杂度也是O(n^2),空间复杂度为O(1)。
n个元素,在1~n-1此比较中,需要每次针对i ,插入到之前的 0 ~ i-1 的位置中。
下面程序:递增排序
public class InsertSort { public static void insertSort(int[] arr) { if(arr == null || arr.length == 0) return ; for(int i=1; i<arr.length; ++i )//假设第一个数位置时正确的;要往后移,必须要假设第一个。 int j = i; int target = arr[i]; //待插入的位置,取出保存 //“待插入”元素位置前面的元素要保持有序处理,并确定“插入排序的位置” //递增排序,从j 到 1的位置比较 // “待插入”元素前面“必然”已经递增排序 while(j > 0 & target < arr[j-1]) { arr[j] = arr[j-1]; j --; } if(j != i){ //待插入位置,有变化,则放入‘合适’的位置 arr[j] = target; } } } }
6. 堆排序
适用场景:
(1)选取前面M个元素,且 待排序的元素很多,或者 Mutiway 情况几乎不能全部读入内存全拍处理。
(2)适应于内存空间非常紧张的情况
(3)是所有排序算法中唯一同时优化“时间”和“空间”的排序算法。
在最坏的情况下,能保证 2NlnN 的比较,且 恒定的 N空间
缺点:
(1)因为堆排序中不能利用CPU缓存,因为堆空间上比较的元素不相互临近。
堆排序是借助堆来实现的选择排序,思想同简单的选择排序,以下以大顶堆为例。
注意:如果想升序排序就使用大顶堆(堆父大于子),反之使用小顶堆(堆父小于子)。原因是堆顶元素需要交换到序列尾部。
首先,实现堆排序需要解决两个问题:
1. 如何由一个无序序列键成一个堆?(堆的构建)
2. 如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?(堆的排序)
第一个问题,可以直接使用线性数组来表示一个堆,由初始的无序序列建成一个堆就需要自底向上从第一个非叶元素开始挨个调整成一个堆(还有更贱有效的算法,即从N/2的位置 到1的位置,跳过堆大小为1的堆判定)。
第二个问题,怎么调整成堆?首先是将堆顶元素和最后一个元素交换。然后比较当前堆顶元素的左右孩子节点,因为除了当前的堆顶元素,左右孩子堆均满足条件,这时需要选择当前堆顶元素与左右孩子节点的较大者(大顶堆)交换,直至叶子节点。我们称这个自堆顶自叶子的调整成为筛选。
从一个无序序列建堆的过程就是一个反复筛选的过程。若将此序列看成是一个完全二叉树,则最后一个非终端节点是n/2取底个元素,由此筛选即可(跳过堆大小为1的堆,减少比较次数)。举个栗子:
49,38,65,97,76,13,27,49序列的堆排序建初始堆和调整的过程如下:
N=8 , N/2=4, 从数组A【4】开始
堆的构建过程如下图:
堆的排序过程如下图:
实现代码:
public class HeapSort { /** * 堆筛选,除了start之外,start~end均满足大顶堆(堆父最大)的定义。 * 调整之后start~end称为一个大顶堆。 * @param arr 待调整数组, 保留的位置0的元素 * @param start 起始指针 * @param end 结束指针 */
//调整堆 public static void heapAdjust(int[] arr, int start, int end) { int temp = arr[start]; //由上向下“下沉” for(int i=2*start+1; i<= end;) { //左右孩子的节点分别为2*i+1,2*i+2 //选择出左右孩子较小的下标 if(arr[i] > arr[i+1]) { i++; } if(temp >= arr[i]) { break; //已经为大顶堆,=保持稳定性。 } arr[start] = arr[i]; //将子节点上移 start = i; //下一轮筛选 } arr[start] = temp; //插入正确的位置 } // 堆排序的入口,是用0位置的元素处理 public static void heapSort(int[] arr) { if(arr == null || arr.length == 0) return ; //建立大顶堆,构建堆 for(int i=arr.length/2; i>=0; i--) {
//对当前i下面的堆判定处理 heapAdjust(arr, i, arr.length-1); } //堆排序阶段, for(int i=arr.length-1; i>=0; i--) { swap(arr, 0, i); heapAdjust(arr, 0, i-1); } } public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
7. 希尔排序
希尔排序是插入排序的一种高效率的实现,也叫缩小增量排序。
简单的插入排序中,如果待排序列是正序时,时间复杂度是O(n),如果序列是基本有序的,使用直接插入排序效率就非常高。
希尔排序就利用了这个特点。
基本思想是:
先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序
复杂度分析:
希尔排序的分析是复杂的,时间复杂度是所取增量的函数,这涉及一些数学上的难题。但是在大量实验的基础上推出当n在某个范围内时,时间复杂度可以达到O(n^1.3)。
举个栗子:
从上述排序过程可见,希尔排序的特点是,子序列的构成不是简单的逐段分割,而是将某个相隔某个增量d的记录组成一个子序列。如上面的例子,第一堂排序时的增量为5,第二趟排序的增量为3。由于前两趟的插入排序中记录的关键字是和同一子序列中的前一个记录的关键字进行比较,因此关键字较小的记录就不是一步一步地向前挪动,而是跳跃式地往前移,从而使得进行最后一趟排序时,整个序列已经做到基本有序,只要作记录的少量比较和移动即可。因此希尔排序的效率要比直接插入排序高。
实现代码:
1 void ShellSort(int arr[], size_t N) 2 { 3 int h = 1; 4 //寻找合适的d(步进量)值 5 while (h < N / 3) 6 h = h * 3 + 1; 7 //shell排序关键component 8 while (h >= 1) 9 { 10 //将当前步进量h下,将每个数组变为h有序 11 for (int i = h; i < N; i++) 12 { 13 //每一个小组,从后向前分别处理,插入排序到前面有序数组 14 for (int j = i; j >= h && less(arr[j], arr[j - h]); j -= h) 15 { 16 //小组内,插入排序处理 17 exchange(arr, j, j - h); 18 } 19 } 20 //修改步进量的值 21 h = h / 3; 22 } 23 }
8. 计数排序
算法应用的条件: O(k)< O(n*log(n))
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。
当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)), 如归并排序,堆排序)
算法思想
计数排序对输入的数据有附加的限制条件:1、输入的线性表的元素属于有限偏序集S;2、设输入的线性表的长度为n,|S|=k(表示集合S中元素的总数目为k),则k=O(n)。在这两个条件下,计数排序的复杂性为O(n)。计数排序的基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。例如,如果输入序列中只有17个元素的值小于x的值,则x可以直接存放在输出序列的第18个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,因此,上述方案还要作适当的修改。
假设输入的线性表L的长度为n,L=L1,L2,..,Ln;线性表的元素属于有限偏序集S,|S|=k且k=O(n),S={S1,S2,..Sk};则计数排序可以描述如下:1、扫描整个集合S,对每一个Si∈S,找到在线性表L中小于等于Si的元素的个数T(Si);2、扫描整个线性表L,对L中的每一个元素Li,将Li放在输出线性表的第T(Li)个位置上,并将T(Li)减1。
#include <iostream> using namespace std; const int MAXN = 100000; const int k = 1000; // range,所有输入的有限偏序集合的范围 int a[MAXN]输入数组, c[MAXN]线性表, ranked[MAXN]; int main() { int n; cin >> n; for (int i = 0; i < n; ++i) { cin >> a[i];
//对以元素值为下标的“线性表L”递增。 ++c[a[i]]; }
//对线性表c根据有限偏序K,依次统计前序“个数” for (int i = 1; i < k; ++i) c[i] += c[i-1];
//以排序元素值 作为下标的线性表中的值“位置个数”,重新定标 定映射 for (int i = n-1; i >= 0; --i) ranked[--c[a[i]]] = a[i];
//根据排序线性表,依次输出对应“已经排序好的元素 for (int i = 0; i < n; ++i) cout << ranked[i] << endl; return 0; }
9. 桶排序
桶排序算是计数排序的一种改进和推广,但是网上有许多资料把计数排序和桶排序混为一谈。其实桶排序要比计数排序复杂许多。
全依赖“比较”操作的排序算法时间复杂度的一个下界O(N*logN)。但确实存在更快的算法。这些算法并不是不用“比较”操作,也不是想办法将比较操作的次数减少到 logN。而是利用对待排数据的某些限定性假设 ,来避免绝大多数的“比较”操作。桶排序就是这样的原理。
基本思想
假设有一组长度为N的待排关键字序列K[1....n]。
首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]....B[M]中的全部内容即是一个有序序列。
这是一个前述几种典型排序算法的综合:(1)快速排序,(2)“有序”映射函数,各个桶之间有序划分
[桶—关键字]映射函数
Bindex=f(key) 其中,bindex 为桶数组B的下标(即第bindex个桶), k为待排序列的关键字。桶排序之所以能够高效,其关键在于这个映射函数,它必须做到:如果关键字k1<k2,那么f(k1)<=f(k2)。也就是说B(i)中的最小数据都要大于B(i-1)中最大数据(桶与桶之间有序)。很显然,映射函数的确定与数据本身的特点有很大的关系,我们下面举个例子:假如待排序列K= {49、 38 、 35、 97 、 76、 73 、 27、 49 }。这些数据全部在1—100之间。因此我们定制10个桶,然后确定映射函数f(k)=k/10。则第一个关键字49将定位到第4个桶中(49/10=4)。依次将所有关键字全部堆入桶中,并在每个非空的桶中进行快速排序后得到如下图所示,对上图只要顺序输出每个B[i]中的数据就可以得到有序序列了。
桶排序代价分析
桶排序利用函数的映射关系(有序分治),减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。
对N个关键字进行桶排序的时间复杂度分为两个部分:
(1) 循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)。
(2) 利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为 ∑ O(Ni*logNi) 。其中Ni 为第i个桶的数据量。
很显然,第(2)部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(N*logN)了)。因此,我们需要尽量做到下面两点:
(1) 映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。
(2) 尽量的增大桶的数量M。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。 当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。
对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:
O(N)+O(M*(N/M)*log(N/M))=O(N+N*(logN-logM))=O(N+N*logN-N*logM)
当N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。对,就是极限条件下。
总结: 桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。 当然桶排序的空间复杂度 为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。
其实我个人还有一个感受:在查找算法中,基于比较的查找算法最好的时间复杂度也是O(logN)。比如折半查找、平衡二叉树、红黑树等。但是Hash表却有O(C)线性级别的查找效率(不冲突情况下查找效率达到O(1))。大家好好体会一下:Hash表的思想和桶排序是不是有一曲同工之妙呢?
桶排序在海量数据中的应用
一年的全国高考考生人数为500 万,分数使用标准分,最低100 ,最高900 ,没有小数,你把这500 万元素的数组排个序。
分析:对500W数据排序,如果基于比较的先进排序,平均比较次数为O(5000000*log5000000)≈1.112亿。但是我们发现,这些数据都有特殊的条件: 100=<score<=900。那么我们就可以考虑桶排序这样一个“投机取巧”的办法、让其在毫秒级别就完成500万排序。
方法:创建801(900-100)个桶。将每个考生的分数丢进f(score)=score-100的桶中。这个过程从头到尾遍历一遍数据只需要500W次。然后根据桶号大小依次将桶中数值输出,即可以得到一个有序的序列。而且可以很容易的得到100分有***人,501分有***人。
实际上,桶排序对数据的条件有特殊要求,如果上面的分数不是从100-900,而是从0-2亿,那么分配2亿个桶显然是不可能的。所以桶排序有其局限性,适合元素值集合并不大的情况。
算法实现:
桶内数据排序,我们使用了基于单链表的直接插入排序算法。可以使用基于双向链表的快排算法提高效率。
#include<iostream.h> #include<malloc.h> typedef struct node{ int key; struct node * next; }KeyNode; void inc_sort(int keys[],int size,int bucket_size){ KeyNode **bucket_table=(KeyNode **)malloc(bucket_size*sizeof(KeyNode *)); for(int i=0;i<bucket_size;i++){ bucket_table[i]=(KeyNode *)malloc(sizeof(KeyNode)); bucket_table[i]->key=0; //记录当前桶中的数据量 bucket_table[i]->next=NULL; } for(int j=0;j<size;j++){ KeyNode *node=(KeyNode *)malloc(sizeof(KeyNode)); node->key=keys[j]; node->next=NULL; //映射函数计算桶号 int index=keys[j]/10; //初始化P成为桶中数据链表的头指针 KeyNode *p=bucket_table[index]; //该桶中还没有数据 if(p->key==0){ bucket_table[index]->next=node; (bucket_table[index]->key)++; }else{ //链表结构的插入排序 while(p->next!=NULL&&p->next->key<=node->key) p=p->next; node->next=p->next; p->next=node; (bucket_table[index]->key)++; } } //打印结果 for(int b=0;b<bucket_size;b++) for(KeyNode *k=bucket_table[b]->next; k!=NULL; k=k->next) cout<<k->key<<" "; cout<<endl; } void main(){ int raw[]={49,38,65,97,76,13,27,49}; int size=sizeof(raw)/sizeof(int); inc_sort(raw,size,10); }
10. 基数排序
基数排序又是一种和前面排序方式不同的排序方式,基数排序不需要进行记录关键字之间的比较。
基数排序是一种借助多关键字排序思想对单逻辑关键字进行排序的方法。所谓的多关键字排序就是有多个优先级不同的关键字。比如说成绩的排序,如果两个人总分相同,则语文高的排在前面,语文成绩也相同则数学高的排在前面。。。如果对数字进行排序,那么个位、十位、百位就是不同优先级的关键字,如果要进行升序排序,那么个位、十位、百位优先级一次增加。基数排序是通过多次的收分配和收集来实现的,关键字优先级低的先进行分配和收集。
public class RadixSort { public static void radixSort(int[] arr) { if(arr == null & arr.length == 0) return ; //获取最大的长度位数 int maxBit = getMaxBit(arr); for(int i=1; i<= maxBit; i++) { <List> buf = distribute(arr, i); //分配 collecte(arr, buf); //收集 } } /** * 分配阶段 * @param arr 待分配数组 * @param iBit 要分配第几位 * @return */ public static List> distribute(int[] arr, int iBit) { List> buf = new ArrayList>(); for(int j=0; j) { buf.add(new LinkedList()); } for(int i=0; i) { buf.get(getNBit(arr[i], iBit)).add(arr[i]); } return buf; } /** * 收集阶段 * @param arr 把分配的数据收集到arr中 * @param buf */ public static void collecte(int[] arr, List> buf) { int k = 0; for(List bucket : buf) { for(int ele : bucket) { arr[k++] = ele; } } } /** * 获取最大位数 * @param x * @return */ public static int getMaxBit(int[] arr) { int max = Integer.MIN_VALUE; for(int ele : arr) { int len = (ele+"").length(); if(len > max) max = len; } return max; } /** * 获取x的第n位,如果没有则为0. * @param x * @param n * @return */ public static int getNBit(int x, int n) { String sx = x + ""; if(sx.length() n) return 0; else return sx.charAt(sx.length()-n) - '0'; } }
各种排序算法的总结,放在另一篇博客中。
参考网址:
http://www.techug.com/sort-algorithm-in-interview
http://hxraid.iteye.com/blog/646760
http://hxraid.iteye.com/blog/647759
浙公网安备 33010602011771号