虽然排序算法是一个简单的问题,但绝对是笔试面试的基础考点,重重之重。来个排序问题都没回答出来,留给面试官的印象也就那样了。 排序主要分为: 比较排序:快速排序、堆排序、归并排序、插入排序、希尔排序、选择排序、冒泡排序 非比较排序:基数排序、计数排序、桶排序 性能比较点: 时间复杂度:一般而言,好的性能是O(nlgn),且坏的性能是O(n^2)。对于一个排序理想的性能是O(n) 稳定性:是否能让原本有相等键值的纪录维持相对次序。 一、插入排序 《算法导论》的第2章介绍了插入排序及其算法分析。 核心:有序序列+直接插入 描述:维持一个有序区,将无序区的第一个元素直接插入到有序区,形成新的有序序列,最终实现排序。最优、平均、最差时间复杂度为θ(n^2)。 算法步骤为: 1、 从第一个元素开始,该元素可以认为已经被排序 2、 取出下一个元素,在已经排序的元素序列中从后向前扫描 3、 如果该元素(已排序)大于新元素,将该元素移到下一位置 4、 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置 5、 将新元素插入到该位置后 6、 重复步骤2~5 上图: 伪代码为: [cpp] view plain copy INSERTION-SORT(A) for j <- 2 to length[A] do key <- A[j] Insert A[j] into the sorted sequence A[1..j-1] i <- j-1 while i>0 and A[i]>key do A[i+1] <- A[i] i <- i-1 A[i+1] <- key 实现: [cpp] view plain copy #include<assert.h> #include<iostream> #include<algorithm> #include<iterator> usingnamespace std; voidinsert_sort(int *a,int len){ assert(a!=NULL && len>0); int key=0,i=0; for(int pos=1;pos<len;++pos){ key = a[pos]; i = pos-1; while(i>=0 && a[i]>key){//backward a[i+1]=a[i]; i--; } a[i+1] = key; } } int main(){ int seq[]={3,7,8,5,2,1,9,5,4}; int length=sizeof(seq)/sizeof(int); copy(seq,seq+length,ostream_iterator<int>(cout,"")); cout<<endl; insert_sort(seq,length); copy(seq,seq+length,ostream_iterator<int>(cout,"")); cout<<endl; return 0; } 结果: Eg. 请写出链表的插入排序程序 (copy过来的) [cpp] view plain copy template<typenameT> structlist_node{ struct list_node<T> *next; T value; }; template<typenameT> struct _list{ struct list_node<T> *head; int size; }; template<typenameT> voidSortLink(struct _list<T> * link) { struct list_node<T>*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; } } 二、二分查找排序 二分查找排序是插入排序的一个变种。改进点:对有序区从末尾一个一个直接比较,改为效率更高的二分查找。在速率上有一定的提升。二分插入排序元素移动次数与直接插入排序相同,最佳情况O(nlgn),最差和平均情况O(n^2) 实现: [cpp] view plain copy voidbinary_insert_sort(int *a,int len){ assert(a!=NULL && len>0); int begin=0,end=0,middle=0; int key=0,i=0; for(int pos=1;pos<len;++pos){ key = a[pos]; begin=0; end=pos-1; while(begin<=end){ middle = (begin+end)/2; if(a[middle]>key) end=middle-1; else begin=middle+1; } i=pos-1; while(i>=begin){ a[i+1]=a[i]; --i; } a[begin] = key; } } 三、希尔排序 Shell sort,递减增量排序算法,因DL.Shell于1959年提出而得名,是插入排序的一种更高效的改进版本。 核心:增量分组+插入排序+增量递减 描述:希尔排序是非稳定排序算法,希尔排序的时间复杂度与增量序列的选取有关,希尔增量时间复杂度为O(n^2)。 步骤: 1、先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中,在各组内进行直接插入排序。 2、取第二个增量d2<d1重复上述的分组和排序, 3、直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。 上图: 伪代码 [cpp] view plain copy input: an array a of length n with array elements numbered 0 to n − 1 inc ← round(n/2) while inc > 0 do: for i = inc .. n − 1 do: temp ← a[i] j ← i while j ≥ inc and a[j − inc]> temp do: a[j] ← a[j − inc] j ← j − inc a[j] ← temp inc ← round(inc / 2) 实现: [cpp] view plain copy #include <assert.h> #include <iostream> #include <algorithm> #include <iterator> using namespace std; void shell_sort(int *a,int len){ assert(a!=NULL && len>0); int key=0; for(int gap=len/2;gap>0;gap/=2){ for(int i=gap;i<len;++i){ key=a[i]; int j=i-gap; while(j>=0 && a[j]>key){ a[j+gap]=a[j]; j-=gap; } a[j+gap]=key; } } int main(){ int seq[]={3,7,8,5,2,1,9,5,4}; int length=sizeof(seq)/sizeof(int); copy(seq,seq+length,ostream_iterator<int>(cout," ")); cout<<endl; cout<<"begin: "<<endl; shell_sort(seq,length); cout<<"end: "<<endl; copy(seq,seq+length,ostream_iterator<int>(cout," ")); cout<<endl; return 0; } 结果 四、选择排序 Selection sort是一种简单直观的排序算法。 核心:有序区+选无序区的极值 描述:将无序区的最值放在有序区的末尾,以此对序列进行排序最好、平均和最坏运行时间为θ(n^2)。 算法步骤为:(此处为递减序列,递增则选无序区的最大值) 1、初始状态:无序区为R[1..n],有序区为空。 2、第i趟排序开始时,当前有序区和无序区分别为R[1..i-1]和R[i…n]。该趟排序从当前无序区中选出关键字最小的记录R[k],将它与无序区的第1个记录R交换,使R[1..i]和R分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。 3、前n-1趟结束,数组有序化了 上图: 伪代码为: [cpp] view plain copy SELECTION-SORT(A) for j = 1 to Length(A) i = j key = A(i) for i to Lenth(A) if key>A(i) key = A(i) k = i A(k) = A(j) A(j) = key 实现: [cpp] view plain copy #include <assert.h> #include <iostream> #include <algorithm> #include <iterator> using namespace std; void select_sort(int *a,int len){ assert(a!=NULL && len>0); int max=0,pos=0; for(int i=0;i<len;++i){ max=a[i]; pos=i; for(int j=i;j<len;++j){ if(a[j]>max){ pos=j; max=a[j]; } } swap(a[i],a[pos]); } } int main(){ int seq[]={3,7,8,5,2,1,9,5,4}; int length=sizeof(seq)/sizeof(int); copy(seq,seq+length,ostream_iterator<int>(cout," ")); cout<<endl; cout<<"begin: "<<endl; select_sort(seq,length); cout<<"end: "<<endl; copy(seq,seq+length,ostream_iterator<int>(cout," ")); cout<<endl; return 0; } 结果: 五、归并排序 《算法导论》的第2章介绍了归并排序及其算法分析,并引入了分治算法策略,divide-and-conquer。 核心:分治 描述:指的是将两个已经排序的串行合并成一个串行的操作。最坏情况下运行时间为θ(n^2),但是平均性能相当好,期望的运行时间为θ(nlgn)。 算法步骤为: 1、 Divide: 把长度为n的输入序列分成两个长度为n/2的子序列 2、 Conquer: 对这两个子序列分别采用归并排序 3、 Combine: 将两个排序好的子序列合并成一个最终的排序序列。 上图: 伪代码为: [cpp] view plain copy MERGE(A,p,q,r) N1←q-p+1 N2←r-q Creat arrays L[1……n1+1] and R[1….n2+1] For i←1 to n1 Do l[i]←A[p+i-1] For j←1 to n2 Do R[j]←A[q+j] L[n1+1]←∞ R[n2+1]←∞ i←1 j←1 for k←p to r do if Li]<=R[j] then A[k]←l[j] i←i+1 else A[k]←R[j] j←j+1 NERGE_SORT(A,p,r) If p<r Then q←[(p+r)/2] MERGE_SORT(A,p,q) MERGE_SORT(A,p+1,q) MERGE_SORT(A,p,q,r) 实现 [cpp] view plain copy #include<assert.h> #include<iostream> #include<algorithm> #include<iterator> usingnamespace std; //combinea[begin,middle] with a[middle+1,end] voidcombine_array(int *a,int b,int m,int e,int *temp){ assert(a!=NULL && b>=0 &&m>=0 && e>=0 && temp!=NULL); int i=b,j=m+1,pos=0; while(i<=m && j<=e){ if(a[i]<=a[j]) temp[pos++]=a[i++]; else temp[pos++]=a[j++]; } while(i<=m) temp[pos++]=a[i++]; while(j<=m) temp[pos++]=a[j++]; for(i=0;i<pos;++i){ a[b+i]=temp[i]; } } voidmerge_sort(int *a,int begin,int end,int *temp){ assert(a!=NULL && begin>=0&& end>=0 && temp!=NULL); if(begin<end){ int middle = (begin+end)/2; merge_sort(a,begin,middle,temp);//left merge_sort(a,middle+1,end,temp);//rigth combine_array(a,begin,middle,end,temp);//combine } } int main(){ int seq[]={3,7,8,5,2,1,9,5}; int length=sizeof(seq)/sizeof(int); int *t = new int(length); copy(seq,seq+length,ostream_iterator<int>(cout,"")); cout<<endl; cout<<"begin: "<<endl; merge_sort(seq,0,length-1,t); cout<<"end: "<<endl; copy(seq,seq+length,ostream_iterator<int>(cout,"")); cout<<endl; delete t; return 0; } 结果: 六、冒泡排序 Bubble sort是一种简单的排序算法。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。 核心:比大小 描述:冒泡排序是与插入排序拥有相等的执行时间。最优O(n),平均、最初O(n^2)。 步骤 1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。 2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。 3、针对所有的元素重复以上的步骤,除了最后一个。持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 上图 伪代码 [cpp] view plain copy function bubblesort (A : list[0..n-1]) { var inti, j; for i fromn-1 downto 0 { for j from 0 to i { if(A[j] > A[j+1]) swap(A[j], A[j+1]) } } } 实现 [cpp] view plain copy #include<assert.h> #include<iostream> #include<algorithm> #include<iterator> usingnamespace std; voidbubble_sort(int *a,int len){ assert(a!=NULL && len>0); for(int i=0;i<=len-1;++i){ for(int j=0;j<=len-1-i;++j) if(a[j]>a[j+1]) swap(a[j],a[j+1]); } } int main(){ int seq[]={3,7,8,5,2,1,9,5,4}; int length=sizeof(seq)/sizeof(int); copy(seq,seq+length,ostream_iterator<int>(cout,"")); cout<<endl; cout<<"begin: "<<endl; bubble_sort(seq,length); cout<<"end: "<<endl; copy(seq,seq+length,ostream_iterator<int>(cout,"")); cout<<endl; return 0; } 结果 七、快速排序 算法导论的第七章介绍了快速排序及其算法分析。 核心:分治+递归 描述:快速排序采用的是分治算法思想,分而治之,各个击破。最坏情况下运行时间为θ(n^2),但是平均性能相当好,期望的运行时间为θ(nlgn)。 算法步骤为: 1、pivot:从数列中挑出一个元素作为基准 2、partition:重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。 3、recursive:把两个子序列递归排序 上图: 伪代码为: [cpp] view plain copy function quicksort(q) var list less,pivotList, greater if length(q) ≤ 1 { return q } else { select a pivot value pivotfrom q for each x inq except the pivot element if x < pivot thenadd x to less if x ≥ pivot thenadd x to greater add pivot topivotList returnconcatenate(quicksort(less), pivotList, quicksort(greater)) } 实现:快排的基准值可以以多种方式获得。取首元素,末尾元素,或者干脆来个随机取值。 维基的百科的实现非常经典,代码如下 [cpp] view plain copy struct Range{ explicit Range(int s=0,int e=0):start(s),end(e){} int start,end; }; void quicksort(int n,int arr[]){ if(n<=0) return; stack<Range> st; st.push(Range(0,n-1)); while(!st.empty()){ Range range = st.top(); st.pop(); int pivot = arr[range.end]; int pos = range.start-1; for(int i=range.start;i<range.end;++i){ if(arr[i]<pivot){ std::swap(arr[i],arr[++pos]); } } std::swap(arr[++pos],arr[range.end]); if(pos-1>range.start){ st.push(Range(range.start,pos-1)); } if(pos+1<range.end){ st.push(Range(pos+1,range.end)); } } } 自己的实现代码,加了个判断,相等就不交换: [cpp] view plain copy #include<assert.h> #include<iostream> #include<algorithm> #include<iterator> usingnamespace std; voidquick_sort(int *a,int len){ assert(a); int pivot=0,low=0,pos=0; if(len>1){ pivot = a[len-1]; for(pos=0,low=0;pos<len-1;++pos){ if(a[pos]<pivot){ if(a[pos]==a[low]){ ++low; continue; } swap(a[pos],a[low++]); } } swap(a[low],a[len-1]); quick_sort(a,low); quick_sort(a+low+1,len-low-1); } } int main(){ int seq[]={3,7,8,5,2,1,9,5,4}; int length=sizeof(seq)/sizeof(int); copy(seq,seq+length,ostream_iterator<int>(cout,"")); cout<<endl; quick_sort(seq,length); copy(seq,seq+length,ostream_iterator<int>(cout,"")); cout<<endl; return 0; } 运行结果: 八、堆排序 《算法导论》的第6章引入了堆、最大堆、最小堆的概念,由此引入了堆排序。Heap sort是利用数据结构堆所设计的一种排序算法。堆的性质是即子结点的键值或索引总是小于(或者大于)它的父节点。 核心:最大(小)堆+建立堆+堆调整 堆节点的性质:父节点i的左子节点在位置 (2*i+1);父节点i的右子节点在位置 (2*i+2);子节点i的父节点在位置 floor((i-1)/2); 描述,最优、平均和最差时间复杂度O(nlgn) 最大堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点 从图中可以看出,在节点i=2时,不满足最大堆的要求,需要进行调整,选择节点2的左右孩子中最大一个进行交换,然后检查交换后的节点i=4是否满足最大堆的要求,从图看出不满足,接着进行调整,直到没有交换为止。 递归形式: [cpp] view plain copy voidadjust_max_heap_recursive(int *datas,int length,int i){ int left,right,largest; int temp; left = LEFT(i); //left child right = RIGHT(i); //right child //find the largest value among left andrihgt and i. if(left<=length && datas[left]> datas[i]) largest = left; else largest = i; if(right <= length &&datas[right] > datas[largest]) largest = right; //exchange i and largest if(largest != i){ temp = datas[i]; datas[i] = datas[largest]; datas[largest] = temp; //recursive call the function,adjustfrom largest adjust_max_heap(datas,length,largest); } } 非递归形式: [cpp] view plain copy voidadjust_max_heap(int *datas,int length,int i){ int left,right,largest; int temp; while(1){ left = LEFT(i); //left child right = RIGHT(i); //right child //find the largest value among left andrihgt and i. if(left <= length &&datas[left] > datas[i]) largest = left; else largest = i; if(right <= length &&datas[right] > datas[largest]) largest = right; //exchange i and largest if(largest != i){ temp = datas[i]; datas[i] = datas[largest]; datas[largest] = temp; i = largest; continue; } else break; } } 创建最大堆(Build_Max_Heap):将堆所有数据重新排序,从最后一个非叶子节点(n/2)开始调整。 [cpp] view plain copy voidbuild_max_heap(int *datas,int length) { int i; //build max heap from the last parent node for(i=length/2;i>0;i--) adjust_max_heap(datas,length,i); } 堆排序(HeapSort):第一个数据的根节点与最后一个节点交换,堆长度减1,并做最大堆调整的递归运算 (1)创建最大堆,数组第一个元素最大,执行后结果下图: (2)进行循环,从length(a)到2,并不断的调整最大堆,给出一个简单过程如下: 排序函数: [cpp] view plain copy voidheap_sort(int *datas,int length){ int i,temp; //bulid max heap build_max_heap(datas,length); i=length; //exchange the first value to the lastunitl i=1 while(i>1){ temp = datas[i]; datas[i] = datas[1]; datas[1] =temp; i--; //adjust max heap,make sure the fisrtvalue is the largest adjust_max_heap(datas,i,1); } } 结果 [cpp] view plain copy #include<iostream> usingnamespace std; void sift(intd[], int ind, int len){ //#置i为要筛选的节点#% int i = ind; //#c中保存i节点的左孩子#% int c = i * 2 + 1; //#+1的目的就是为了解决节点从0开始而他的左孩子一直为0的问题#% while(c < len)//#未筛选到叶子节点#%{ //#如果要筛选的节点既有左孩子又有右孩子并且左孩子值小于右孩子#% //#从二者中选出较大的并记录#% if(c + 1 < len && d[c] <d[c + 1]) c++; //#如果要筛选的节点中的值大于左右孩子的较大者则退出#% if(d[i] > d[c]) break; else{ //#交换#% int t = d[c]; d[c] = d[i]; d[i] = t; // //#重置要筛选的节点和要筛选的左孩子#% i = c; c = 2 * i + 1; } } return; } voidheap_sort(int d[], int n){ //#初始化建堆, i从最后一个非叶子节点开始#% for(int i = (n - 2) / 2; i >= 0; i--) sift(d, i, n); for(int j = 0; j < n; j++){ //#交换#% int t = d[0]; d[0] = d[n - j - 1]; d[n - j - 1] = t; //#筛选编号为0 #% sift(d, 0, n - j - 1); } } int main(){ int a[] = {4,1,3,16,9,10,14,8,7}; heap_sort(a, sizeof(a) / sizeof(*a)); for(int i = 0; i < sizeof(a) /sizeof(*a); i++){ cout << a[i] << ' '; } cout << endl; return 0; }