和很多计算机系的同学们一样,我在大学二年级时也学了《数据结构》这门课。当时我的老师是一个中科大的博士,现在已经是教授了。他在课上曾经这样评价这门课:《数据结构》几乎是所有计算机课程的基础课,如果把这门课学好了,其他的专业课就不成问题了。还有,IT公司的面试经常涉及到数据结构的相关知识,该课程的重要性由此可见。但是当时年少无知根本没好好学习,等到笔试,面试时才幡然悔悟。下面的内排序算法可算是数据结构中的重要内容,程序代码全部用C++实现,已在visual C++6.0上运行过了。
一.插入排序(insert sorting)
最差情况下,直接插入排序的最大时间代价为θ(n²),最小时间代价为θ(n),平均时间代价为θ(n²)。
#include <iostream> using namespace std; /*交换函数,作用是交换数组中的两个元素的位置*/ void swap(int array[],int i,int j) { int tmp=array[i]; array[i]=array[j]; array[j]=tmp; } /*插入排序*/ void InsertSort(int array[],int n) { for(int i=1;i<n;i++) { for(int j=i;j>0;j–) { if(array[j]>array[j-1]) swap(array,j,j-1); else break; } } } int main() { int array[5]={3,1,2,5,4}; InsertSort(array,5); for(int i=0;i<5;i++) cout<<array[i]<<” “; cout<<endl; return 0; }
二.冒泡排序(bubble sorting)
冒泡排序的最大时间代价,最小时间代价和平均时间代价均为θ(n²)。
#include <iostream> using namespace std; /*交换函数,作用是交换数组中的两个元素的位置*/ void swap(int array[],int i,int j) { int tmp=array[i]; array[i]=array[j]; array[j]=tmp; } /*冒泡排序*/ void BubbleSort(int array[],int n) { for(int i=0;i<n-1;i++) { for(int j=n-1;j>i;j–) { if(array[j]<array[j-1]) swap(array,j,j-1); } } } int main() { int array[5]={3,1,2,5,4}; BubbleSort(array,5); for(int i=0;i<5;i++) cout<<array[i]<<” “; cout<<endl; return 0; }
三.选择排序(selection sorting)
选择排序的最大时间代价,最小时间代价和平均时间代价均为θ(n²)。选择排序不依赖于原始数组的输入顺序。
#include <iostream> using namespace std; /*交换函数,作用是交换数组中的两个元素的位置*/ void swap(int array[],int i,int j) { int tmp=array[i]; array[i]=array[j]; array[j]=tmp; } /*选择排序*/ void SelectionSort(int array[],int n) { for(int i=0;i<n-1;i++) { int smallest=i; for(int j=i+1;j<n;j++) { if(array[smallest]>array[j]) smallest=j; } swap(array,i,smallest); } } int main() { int array[5]={3,1,2,5,4}; SelectionSort(array,5); for(int i=0;i<5;i++) cout<<array[i]<<” “; cout<<endl; return 0; }
四.shell sorting
增量为2的shell排序的时间代价可以达到θ(n的3/2次方),有的增量可以达到θ(n的7/6次方),很接近θ(n)。
#include <iostream> using namespace std; /*交换函数,作用是交换数组中的两个元素的位置*/ void swap(int array[],int i,int j) { int tmp=array[i]; array[i]=array[j]; array[j]=tmp; } /*希尔排序*/ void ShellSort(int array[],int n) { for(int delta=n/2;delta>0;delta/=2) { for(int i=0;i<delta;i++) { for(int j=i+delta;j<n;j+=delta) { for(int k=j;k>0;k-=delta) { if(array[k]<array[k-1]) swap(array,k,k-1); } } } } } int main() { int array[8]={6,8,7,3,1,2,5,4}; ShellSort(array,8); for(int i=0;i<8;i++) cout<<array[i]<<” “; cout<<endl; return 0; }
五.快速排序(quick sorting)
快速排序的最大时间代价为θ(n²),最小时间代价为θ(n*logn),平均时间代价为θ(n*logn)。注意:快速排序是一种不稳定的排序方式,其性能依赖于原始数组的有序程度,更进一步分析,就是依赖与轴值元素的选择。快排的比较次数远多于移动次数,所以主要考虑比较次数。
快排中,每一次比较可以确定一个轴值元素的位置。若p[m,q,n](q为轴值元素)。当然确定第一个轴值元素也是要比较(n-m-1)次。但第二个轴值元素,第三个轴值元素就要进行(q-m-1)和(n-q-1)次比较。如果q的值若为m或n,快速排序就退化成冒泡排序了,快排就没有什么优势了。
#include <iostream> using namespace std; /*交换函数,作用是交换数组中的两个元素的位置*/ void swap(int array[],int i,int j) { int tmp=array[i]; array[i]=array[j]; array[j]=tmp; } /*将轴值放到数组的适当的位置*/ int partition(int array[],int left,int right) { int mid=(left+right)/2; int tmp=array[mid]; swap(array,mid,right); int i=left; int j=right; while(1) { /*i指针向右移动,直到找到一个大于轴值的值*/ while(1) { /*如果i与j相遇则确定轴值位置,将其返回*/ if(i==j) { array[i]=tmp; return i; } if(array[i]>tmp) { array[j]=array[i]; j–; break; } i++; } /*j指针向左移动,直到找到一个小于轴值的值*/ while(1) { /*如果i与j相遇则确定轴值位置,将其返回*/ if(i==j) { array[j]=tmp; return j; } if(array[j]<tmp) { array[i]=array[j]; i++; break; } j–; } } } /*快速排序*/ void quickSort(int array[],int left,int right) { if(right<=left) return; int pivot=partition(array,left,right); quickSort(array,left,pivot-1); quickSort(array,pivot+1,right); } int main() { int array[8]={6,8,7,3,1,2,5,4}; quickSort(array,0,7); for(int i=0;i<8;i++) cout<<array[i]<<” “; cout<<endl; return 0; }
六.归并排序(merge sorting)
归并排序的最大时间代价,最小时间代价和平均时间代价均为θ(n*logn)。归并排序不依赖于原始数组的有序程度。
#include <iostream> using namespace std; /*归并过程–将两个有序数组合并成一个有序数组*/ void merge(int array[],int tempArray[],int left,int right,int middle) { int index1=left; int index2=middle+1; for(int i=left;(index1<=middle)&&(index2<=right);i++) { if(array[index1]<array[index2]) tempArray[i]=array[index1++]; else tempArray[i]=array[index2++]; } while(index1<=middle) tempArray[i++]=array[index1++]; while(index2<=right) tempArray[i++]=array[index2++]; for(i=left;i<=right;i++) array[i]=tempArray[i]; } /*两路归并排序–array[]为待排数组,tempArray[]为临时数组,left和right分别是数组两端*/ void mergeSort(int array[],int tempArray[],int left,int right) { if(left<right) { int middle=(left+right)/2; mergeSort(array,tempArray,left,middle); mergeSort(array,tempArray,middle+1,right); merge(array,tempArray,left,right,middle); } } int main() { int array[8]={6,8,7,3,1,2,5,4}; int tempArray[8]; mergeSort(array,tempArray,0,7); for(int i=0;i<8;i++) cout<<array[i]<<” “; cout<<endl; return 0; }
七.堆排序(heap sorting)
堆排序的最大时间代价,最小时间代价和平均时间代价均为θ(n*logn)。堆排序和归并排序一样,不依赖于原始数组的有序程度。
HeapSorting.cpp
#include <iostream> #include “MaxHeap.h” using namespace std; /*最大堆排序函数*/ void heapSort(int array[],int n) { MaxHeap max_heap=MaxHeap(array,n); /*删除堆的最大值(堆顶),即每次将最大值与数组的最后一个元素交换位置*/ for(int i=0;i<7;i++) max_heap.removeMax(); } int main() { int array[8]={4,3,7,1,2,8,5,6}; heapSort(array,8); for(int i=0;i<8;i++) cout<<array[i]<<” “; cout<<endl; return 0; }
MaxHeap.h
#include <iostream> using namespace std; /*最大堆定义*/ class MaxHeap { private: int size; //最大堆的元素数目 int * array; //最大堆数组的首地址指针 public: MaxHeap(int array[],int n); //用已有数组初始化一个最大堆 void buildHeap(); //构建最大堆 void siftDown(int index); //向下筛选法 void swap(int index1,int index2); //交换位置为index1与index2的元素 void removeMax(); //删除堆顶的最大值–与数组最后一个元素交换位置并重新构建最大堆 int leftChild(int index); //返回左孩子的位置 int rightChild(int index); //返回右孩子的位置 };
MaxHeap.cpp
#include <iostream> #include “MaxHeap.h” using namespace std; /*最大堆成员函数实现*/ MaxHeap::MaxHeap(int array[],int n) { this->array=array; size=n; buildHeap(); } void MaxHeap::buildHeap() { for(int i=size/2-1;i>=0;i–) siftDown(i); } void MaxHeap::siftDown(int index) { int max_index=leftChild(index); while(max_index<size) { if(max_index<size-1&&array[rightChild(index)]>array[max_index]) max_index++; if(array[index]>array[max_index]) break; swap(index,max_index); index=max_index; max_index=leftChild(index); } } void MaxHeap::swap(int index1,int index2) { int temp=array[index1]; array[index1]=array[index2]; array[index2]=temp; } void MaxHeap::removeMax() { swap(0,size-1); size–; siftDown(0); } int MaxHeap::leftChild(int index) { return index*2+1; } int MaxHeap::rightChild(int index) { return index*2+2; }
八.基数排序(radix sorting)
基数排序的时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的比较性排序法。基数排序法是属于稳定性的排序。
#include <iostream> using namespace std; /*计算关键字位数的最大值*/ int KeySize(int array[],int size) { int key_size=1; for(int i=0;i<size;i++) { int temp=1; int n=10; while(array[i]/n>0) { temp++; n*=10; } key_size=(temp>key_size)?temp:key_size; } return key_size; } /*基数排序*/ void RadixSort(int array[],int size) { int bucket[10][10]={0};//定义基数桶 int order[10]={0};//保存每个基数桶之中的元素个数 int key_size=KeySize(array,size);//计算关键字位数的最大值 for(int n=1;key_size>0;n*=10,key_size–) { /*将待排序的元素按照关键值的大小依次放入基数桶之中*/ for(int i=0;i<size;i++) { int lsd=(array[i]/n)%10; bucket[lsd][order[lsd]]=array[i]; order[lsd]++; } /*将基数桶中的元素重新串接起来*/ int k=0; for(i=0;i<10;i++) { if(order[i]!=0) { for(int j=0;j<order[i];j++) { array[k]=bucket[i][j]; k++; } order[i]=0; } } } } int main() { int array[10]={73,22,93,43,55,14,28,65,39,81}; int size=sizeof(array)/sizeof(int); RadixSort(array,size); for(int i=0;i<size;i++) cout<<array[i]<<” “; cout<<endl; return 0; }
下面我们来讨论一下各种排序算法的稳定度,稳定排序算法会依照相等的关键(换言之就是值)维持纪录的相对次序。也就是一个排序算法是稳定的,就是当有两个有相等关键的纪录R和S,且在原本的串列中R出现在S之前,在排序过的串列中R也将会是在S之前。
一般的方法:插入、交换、选择、合并等等。交换排序包含冒泡排序(bubble sort)和快速排序(quicksort)。选择排序包含shaker排序和堆排序(heapsort)。
当相等的元素是无法分辨的,比如像是整数,稳定度并不是一个问题。然而,假设以下的数对将要以他们的第一个数字来排序。
(4, 1) (3, 1) (3, 7) (5, 6)
在这个状况下,有可能产生两种不同的结果,一个是依照相等的键值维持相对的次序,而另外一个则没有:
(3, 1) (3, 7) (4, 1) (5, 6) (维持次序)
(3, 7) (3, 1) (4, 1) (5, 6) (次序被改变)
不稳定排序算法可能会在相等的键值中改变纪录的相对次序,但是稳定排序算法从来不会如此。不稳定排序算法可以被特别地时作为稳定。作这件事情的一个方式是人工扩充键值的比较,如此在其他方面相同键值的两个物件间之比较,就会被决定使用在原先资料次序中的条目,当作一个同分决赛。然而,要记住这种次序通常牵涉到额外的空间负担。
稳定的排序算法:
冒泡排序(bubble sort) — O(n2)
鸡尾酒排序 (Cocktail sort, 双向的冒泡排序) — O(n2)
插入排序 (insertion sort)— O(n2)
桶排序 (bucket sort)— O(n); 需要 O(k) 额外 记忆体
归并排序 (merge sort)— O(n log n); 需要 O(n) 额外记忆体
原地归并排序 — O(n2)
二叉树排序 (Binary tree sort) — O(n log n); 需要 O(n) 额外记忆体
基数排序 (radix sort)— O(n·k); 需要 O(n) 额外记忆体
不稳定的排序算法:
选择排序 (selection sort)— O(n2)
希尔排序 (shell sort)— O(n log n) 如果使用最佳的现在版本
Comb sort — O(n log n)
堆排序 (heapsort)— O(n log n)
Smoothsort — O(n log n)
快速排序 (quicksort)— O(n log n) 期望时间, O(n2) 最坏情况; 对於大的、乱数串列一般相信是最快的已知排序
一般来说:存在不相邻交换的排序算法是不稳定的,相邻交换的排序算法是稳定的;对于相邻交换的稳定排序算法,通过控制交换条件可以转换成不稳定排序算法;冒泡、插入、归并和基数排序是稳定的;选择、快速、希尔和堆排序是不稳定的。