本辑将会对笔试面试最常涉及到的12种排序算法(包括插入排序、二分插入排序、希尔排序、选择排序、冒泡排序、鸡尾酒排序、快速排序、堆排序、归并排序、桶排序、计数排序和基数排序)进行详解。每一种算法都有基本介绍、算法原理分析、图解演示、算法代码、笔试面试重点分析、笔试面试题等板块。
一、插入排序
1)算法简介
插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
2)算法描述和分析
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
1、从第一个元素开始,该元素可以认为已经被排序
2、取出下一个元素,在已经排序的元素序列中从后向前扫描
3、如果该元素(已排序)大于新元素,将该元素移到下一位置
4、重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5、将新元素插入到该位置后
6、重复步骤2~5
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。
最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。
最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数减去(n-1)次。
平均来说插入排序算法复杂度为O(n^2)。
因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。
3)算法图解
4)算法代码
// 插入排序(直接,折半,路插入,表插入) // 依次取出便利每一项 将每一项
temp
=a[j]和其之前的数进行比较,如果发现了比自己大的数 就将其放在当前j的位置 同时j--,继续用temp和之前的数进行比较,直到没有比自己大的数时结束。 // 直接插入排序 void insertSort(int *a,int size) { if(size<2) return; int temp; int j; for(int i=1;i0&&temp
a[i-1]) //这句话可以稍微优化一下下速度 不影响结果 continue;*/ while (low<=high){ m=(low+high)/2; if (temp<=a[m]) high=m-1; //当程序跳出时 high指向的其实并不是 >=temp的值的位置 而是>=temp的值的左边的位置 所以.... else low=m+1; } for( j=i;j>high+1;--j) //所以 这里的high要加一哇 a[j]=a[j-1]; a[j]=temp; } print(a,size); }
5)考察点,重点和频度分析
把插入排序放在第一个的原因是因为其出现的频度不高,尤其是这里提到的直接排序算法,基本在笔试的选择填空问时间空间复杂度的时候才可能出现。毕竟排序速度比较慢,因此算法大题中考察的次数比较比较少。
6)笔试面试例题
例题1
请写出链表的插入排序程序
template*next; T value; }; template*head; int size; }; template* link) { struct list_node*pHead,*pRear,*p,*tp; if (!link) return; for (pHead=link->head,pRear=0;pHead;pHead=pHead->next) { for (tp=pHead,p=pHead->next;p;tp=p,p=p->next) if (pHead->value>=p->value) tp->next=p->next,p->next=pHead,pHead=p,p=tp; if (!pRear) link->head=pHead; else pRear->next=pHead; pRear=pHead; } }
例题2
下列排序算法中最坏复杂度不是n(n-1)/2的是 D
A.快速排序 B.冒泡排序 C.直接插入排序 D.堆排序
二、二分插入排序
1)算法简介
二分(折半)插入(Binary insert sort)排序是一种在直接插入排序算法上进行小改动的排序算法。其与直接排序算法最大的区别在于查找插入位置时使用的是二分查找的方式,在速度上有一定提升。
2)算法描述和分析
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
1、从第一个元素开始,该元素可以认为已经被排序
2、取出下一个元素,在已经排序的元素序列中二分查找到第一个比它大的数的位置
3、将新元素插入到该位置后
4、重复上述两步
1)稳定
2)空间代价:O(1)
3)时间代价:插入每个记录需要O(log i)比较,最多移动i+1次,最少2次。最佳情况O(n log n),最差和平均情况O(n^2)。
二分插入排序是一种稳定的排序。当n较大时,总排序码比较次数比直接插入排序的最差情况好得多,但比最好情况要差,所元素初始序列已经按排序码接近有序时,直接插入排序比二分插入排序比较次数少。二分插入排序元素移动次数与直接插入排序相同,依赖于元素初始序列。
3)算法图解
4)算法代码
void BinInsertSort(int a, int n) { int key, left, right, middle; for (int i=1; ikey) right = middle-1; else left = middle+1; } for(int j=i-1; j>=left; j--) { a[j+1] = a[j]; } a[left] = key; } }
5)考察点,重点和频度分析
这个排序算法在笔试面试中出现的频度也不高,但毕竟是直接排序算法的一个小改进算法,同时二分查找又是很好的思想,有可能会在面试的时候提到,算法不难,留心一下就会了。
6)笔试面试例题
例题1
下面的排序算法中,初始数据集的排列顺序对算法的性能无影响的是(B)
A、二分插入排序 B、堆排序 C、冒泡排序 D、快速排序
例题2
写出下列算法的时间复杂度。
(1)冒泡排序;(2)选择排序;(3)插入排序;(4)二分插入排序;(5)快速排序;(6)堆排序;(7)归并排序;
三、希尔排序
1)算法简介
希尔排序,也称递减增量排序算法,因DL.Shell于1959年提出而得名,是插入排序的一种高速而稳定的改进版本。
2)算法描述和分析
1、先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。
2、所有距离为d1的倍数的记录放在同一个组中,在各组内进行直接插入排序。
3、取第二个增量d2希尔排序的时间复杂度与增量序列的选取有关。 例如希尔增量时间复杂度为O(n^2),而Hibbard增量的希尔排序的时间复杂度为O(N^(5/4)),但是现今仍然没有人能找出希尔排序的精确下界。
3)算法图解
4)算法代码
#includeint main { const int n = 5; int i, j, temp; int gap = 0; int a = {5, 4, 3, 2, 1}; while (gap<=n) { gap = gap * 3 + 1; } while (gap > 0) { for ( i = gap; i < n; i++ ) { j = i - gap; temp = a[i]; while (( j >= 0 ) && ( a[j] > temp )) { a[j + gap] = a[j]; j = j - gap; } a[j + gap] = temp; } gap = ( gap - 1 ) / 3; } }
5)考察点,重点和频度分析
事实上希尔排序算法在笔试面试中出现的频度也不比直接插入排序高,但它的时间复杂度并不是一个定值,所以偶尔会被面试官问到选择的步长和时间复杂度的关系,要稍微有点了解吧。算法大题中使用该方法或者其思想的题也不多。
6)笔试面试例题例题1
写出希尔排序算法程序,并说明最坏的情况下需要进行多少次的比较和交换。
程序略,需要O(n^2)次的比较
例题2
设要将序列(Q, H, C, Y, P, A, M, S, R, D, F, X)中的关键码按字母序的升序重新排列,则:
冒泡排序一趟扫描的结果是 H, C, Q, P, A, M, S, R, D, F, X ,Y ;
初始步长为4的希尔(shell)排序一趟的结果是 P, A, C, S, Q, D, F, X , R, H,M, Y ;
二路归并排序一趟扫描的结果是 H, Q, C, Y,A, P, M, S, D, R, F, X ;
快速排序一趟扫描的结果是 F, H, C, D, P, A, M, Q, R, S, Y,X ;
堆排序初始建堆的结果是 A, D, C, R, F, Q, M, S, Y,P, H, X 。
四、选择排序
1)算法简介
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
2)算法描述和分析
n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果:
1、初始状态:无序区为R[1..n],有序区为空。
2、第i趟排序(i=1,2,3...n-1)
第i趟排序开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。
3、前n-1趟结束,数组有序化了
选择排序的交换操作介于0和(n-1)次之间。选择排序的比较操作为n(n-1)/2次之间。选择排序的赋值操作介于0和3(n-1)次之间。比较次数O(n^2)
,
比较次数与关键字的初始状态无关
,总的比较次数N=(n-1)+(n-2)+...+1=n*(n-1)/2。
交换次数O(n),
最好情况是,已经有序,交换0次;最坏情况是,逆序,交换n-1次。 交换次数比冒泡排序少多了,由于交换所需
CPU
时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。
最差时间复杂度О(n^2) 最优时间复杂度О(n^2) 平均时间复杂度О(n^2) 最差空间复杂度О(n) total, O(1)
3)算法图解
4)算法代码
void selection_sort(int *a, int len) { register int i, j, min, t; for(i = 0; i < len - 1; i ++) { min = i; //查找最小值 for(j = i + 1; j < len; j ++) if(a[min] > a[j]) min = j; //交换 if(min != i) { t = a[min]; a[min] = a[i]; a[i] = t; } } }
5)考察点,重点和频度分析
就博主看过的笔试面试题而言,选择算法也大多出现在选择填空中,要熟悉其时间和空间复杂度,最好最坏的情况分别是什么,以及在那种情况下,每一轮的比较次数等。
6)笔试面试例题
例题1
在插入和选择排序中,若初始数据基本正序,则选用 插入排序(到尾部) ;若初始数据基本反序,则选用 选择排序。
例题2
下述几种排序方法中,平均查找长度(ASL)最小的是
A. 插入排序 B.快速排序 C. 归并排序 D. 选择排序
五、冒泡排序
1)算法简介
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
2)算法描述
1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3、针对所有的元素重复以上的步骤,除了最后一个。
4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
冒泡排序是与插入排序拥有相等的执行时间,但是两种法在需要的交换次数却很大地不同。在最坏的情况,冒泡排序需要O(n^2)次交换,而插入排序只要最多O(n)交换。冒泡排序的实现(类似下面)通常会对已经排序好的数列拙劣地执行(O(n^2)),而插入排序在这个例子只需要O(n)个运算。因此很多现代的算法教科书避免使用冒泡排序,而用插入排序取代之。冒泡排序如果能在内部循环第一次执行时,使用一个旗标来表示有无需要交换的可能,也有可能把最好的复杂度降低到O(n)。在这个情况,在已经排序好的数列就无交换的需要。若在每次走访数列时,把走访顺序和比较大小反过来,也可以稍微地改进效率。有时候称为往返排序,因为算法会从数列的一端到另一端之间穿梭往返。
最差时间复杂度
O(n^2)
最优时间复杂度
O(n)
平均时间复杂度
O(n^2)
最差空间复杂度
总共O(n),需要辅助空间O(1)
3)算法图解
4)算法代码
#includevoid bubbleSort(int
arr, int count) { int i = count, j; int temp; while(i > 0) { for(j = 0; j < i - 1; j++) { if(arr[j] > arr[j + 1]) { temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } i--; } } int main { //测试数据 int arr = {5, 4, 1, 3, 6}; //冒泡排序 bubbleSort(arr, 5); //打印排序结果 int i; for(i = 0; i < 5; i++) printf(%4d, arr[i]); }
5)考察点,重点和频度分析
一般我们学到的第一个排序算法就是冒泡排序,不得不说,这个还真是一个很常见的考点,平均时间空间复杂度,最好最坏情况下的时间空间复杂度,在不同情况下每一趟的比较次数,以及加标志位减少比较次数等,都是需要注意的地方。
6)笔试面试例题
例题1
对于整数序列100,99,98,…3,2,1,如果将它完全倒过来,分别用冒泡排序,它们的比较次数和交换次数各是多少?
答:冒泡排序的比较和交换次数将最大,都是1+2+…+n-1=n(n-1)/2=50×99=4545次。
例题2
把一个字符串的大写字母放到字符串的后面,各个字符的相对位置不变,不能申请额外的空间。
事实上,这道题放到冒泡排序这里不知道是不是特别合适,只是有一种解法是类似冒泡的思想,如下解法一
解法一
每次遇到大写字母就往后冒,最后结果即为所求
#include#include//题目以及要求:把一个字符串的大写字母放到字符串的后面, //各个字符的相对位置不变,不能申请额外的空间。 //判断是不是大写字母 int isUpperAlpha(char c){ if(c >= 'A' && c <= 'Z'){ return 1; } return 0; } //交换两个字母 void swap(char *a, char *b){ char temp = *a; *a = *b; *b = temp; } char * mySort(char *arr, int len){ if(arr == NULL || len <= 0){ return NULL; } int i = 0, j = 0, k = 0; for(i = 0; i < len; i++){ for(j = len - 1 - i; j >= 0; j--){ if(isUpperAlpha(arr[j])){ for(k = j; k < len - i - 1; k++){ swap(&arr[k], &arr[k + 1]); } break; } //遍历完了字符数组,但是没发现大写字母,所以没必要再遍历下去 if(j == 0 && !isUpperAlpha(arr[j])){ //结束; return arr; } } } //over: return arr; } int main{ char arr = aaaaaaaaaaaaaaaaaaaaaaaAbcAdeBbDc; printf(%s , mySort(arr, strlen(arr))); return 0; }
解法二
步骤如下
1、两个
指针
p1和p2,从后往前扫描
2、p1遇到一个小写字母时停下, p2遇到大写字母时停下,两者所指向的char交换
3、p1, p2同时往前一格
代码如下#include#include//判断是不是大写字母 int isUpperAlpha(char c){ if(c >= 'A' && c <= 'Z'){ return 1; } return 0; } //交换两个字母 void swap(char *a, char *b){ char temp = *a; *a = *b; *b = temp; } char * Reorder(char *arr, int len){ if(arr == NULL || len <= 0){ return NULL; } int *p1 = arr; int *p2 = arr; While(p1
冒泡排序(Bubble Sort)也是一种简单直观的
排序算法
。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个
算法
的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。也就是双重循环就可以搞定的问题但是需要注意下一边界
算法步骤:
1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3)针对所有的元素重复以上的步骤,除了最后一个。
4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。<span style="font-family:
Microsoft YaHei
;font-size:14px;">public void BubbleSort(int a) {
int temp
= 0; int len = a.length; for (int i = 0; i < len; i++) { for (int j = 1; j < len - i; j++) if (a[j - 1] > a[j]) { //注意分清是a[j-1]还是a[j]不然容易出现边界问题 // 从小到大排序 temp = a[j - 1]; a[j - 1] = a[j]; a[j] = temp; } } }</span>
优化的冒泡排序
由于可能在前几次就已经排好序,但是在上一种冒泡排序中仍然需要一直遍历到最后。
优化措施:设置一个标志,如果这一趟发生了交换,则为true,否则为false。明显如果有一趟没有发生交换,说明排序已经完成。
<span style="font-family:Microsoft YaHei;font-size:14px;">public void BubbleSort1(int a) { int temp = 0; int len = a.length; boolean flag = true; while (flag) { flag = false; for (int j = 1; j < len - 1; j++) if (a[j - 1] > a[j]) { // 注意分清是a[j-1]还是a[j]不然容易出现边界问题 // 从小到大排序 temp = a[j - 1]; a[j - 1] = a[j]; a[j] = temp; // 设置标志位 flag = true; } } }</span>
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
算法步骤:
1) 从数列中挑出一个元素,称为 “基准”(pivot),
2 )重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3 )递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。<span style="font-family:Microsoft YaHei;font-size:14px;"> private static void quick_sort(int arr, int low, int high) { // 解决和合并 if (low <= high) { int mid = partition(arr, low, high); // 递归 quick_sort(arr, low, mid - 1); quick_sort(arr, mid + 1, high); } } private static int partition(int arr, int low, int high) { // 分解 int pivot = arr[high]; int i = low - 1; int temp; for (int j = low; j < high; j++) { if (arr[j] < pivot) { i++; temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } // 交换中间元素和privot temp = arr[i + 1]; arr[i + 1] = arr[high]; arr[high] = temp; return i + 1; }</span>
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
算法步骤:
1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
4. 重复步骤3直到某一指针达到序列尾
5. 将另一序列剩下的所有元素直接复制到合并序列尾<span style="font-family:Microsoft YaHei;font-size:14px;"> public static int sort(int nums, int low, int high) { int mid = (low + high) / 2; if (low < high) { // 左边 sort(nums, low, mid); // 右边 sort(nums, mid + 1, high); // 左右归并 merge(nums, low, mid, high); } return nums; } public static void merge(int nums, int low, int mid, int high) { int temp =
new
int[high - low + 1]; int i = low;// 左指针 int j = mid + 1;// 右指针 int k = 0; // 把较小的数先移到新数组中 while (i <= mid && j <= high) { if (nums[i] < nums[j]) { temp[k++] = nums[i++]; } else { temp[k++] = nums[j++]; } } // 把左边剩余的数移入数组 while (i <= mid) { temp[k++] = nums[i++]; } // 把右边边剩余的数移入数组 while (j <= high) { temp[k++] = nums[j++]; } // 把新数组中的数覆盖nums数组 for (int k2 = 0; k2 < temp.length; k2++) { nums[k2 + low] = temp[k2]; } } </span>
选择排序(Selection sort)也是一种简单直观的排序算法。
算法步骤:
1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
2)再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3)重复第二步,直到所有元素均排序完毕。
<span style="font-family:Microsoft YaHei;font-size:14px;"> public int ChoseSort(int intArr){ for(int i=0;i<intArr.length;i++){ int
lowIndex
= i; for(int j=i+1;j<intArr.length;j++){ if(intArr[j]<intArr[lowIndex]){ lowIndex = j; } } //将当前第一个元素与它后面序列中的最小的一个 元素交换,也就是将最小的元素放在最前端 int temp = intArr[i]; intArr[i] = intArr[lowIndex]; intArr[lowIndex] = temp; } return intArr; } </span>
堆排序
(Heapsort)是指利用堆这种
数据结构
所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序的平均时间复杂度为Ο(nlogn) 。
算法步骤:
1)创建一个堆H[0..n-1]
2)把堆首(最大值)和堆尾互换
3)把堆的尺寸缩小1,并调用shift_down(0),目的是把新的数组顶端数据调整到相应位置
4) 重复步骤2,直到堆的尺寸为1
调整堆部分不太好写建议参考
最后给出一张各算法的性能比较图