基本概念:
排序的稳定性:假设ki=kj(1<=i<=n,1<=j<=n,i!=j),且在排序前的序列中ri领先于rj(即i<j)。如果排序后ri依然领先于rj,则称所用的排序算法是稳定的;反之,若可能使得排序后的序列中rj领先ri,则称所用的排序方法是不稳定的。
根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为:内排序和外排序。
内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。外排序是由于排序的记录太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。这里主要介绍的是内排序的多种方法。
排序算法的性能:
1、时间性能:高效率的内排序算法应该是具有尽可能少的关键字比较次数和尽可能少的记录移动次数。2、辅助空间:辅助空间是除了待排序所占用的存储空间之外,执行算法所需要的其他存储空间。内排序分为:插入排序、交换排序、选择排序和归并排序。
算法介绍:
交换两个整型数据的函数swap:
void swap(int *a,int *b) { int temp = *a; *a = *b; *b = temp; }
1、冒泡排序
(1)冒泡排序是一种交换排序,它的基本思想是:两两比较相邻的关键字,如果反序则交换,直到没有反序的记录为止。
代码实现:
冒泡排序的初级版:交换排序
思路:让每一个关键字,都和它后面的每一个关键字比较,如果大则交换,这样第一个位置的关键字在一次循环后一定变成最小值。
void swap_sort(int arr[],int len) { int i,j; for (i=0;i<len-1;++i) { for (j=i+1;j<len;++j) { if (arr[i]>arr[j])//与其余所有元素比较,将最小的数据放在最前面 { swap(&arr[i],&arr[j]); } } } }
冒泡排序的简单算法:
void bubble_sort(int arr[],int len) { int i,j; for (i=0;i<len-1;++i) { for (j=len-1;j>i;j--) { if (arr[j]<arr[j-1]) { swap(&arr[j],&arr[j-1]); } } } }
冒泡排序的升级版:
(2)交换排序的时间复杂度分析:void bubble_sort_plus(int arr[],int len) { int i,j; for (i=0;i<len-1;++i) { int flag = true; for (j=len-1;j>i;j--) { if (arr[j]<arr[j-1]) { swap(&arr[j],&arr[j-1]); flag = false; } } if (flag) { break; } } }
当最好的情况,也就是排序的表本身是有序的,那么我们的比较次数,根据最后改进的代码,可以推断出就是n-1次的比较,没有数据交换,时间复杂度为O(n)。当最坏的情况,即待排序是逆序的情况,此时需要比较n(n-1)/2次,并作等数量级的记录移动。因此,总的时间复杂度是O(n2)。
2、简单选择排序
(1)简单选择排序法就是通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1≤i≤n)个记录交换之。
代码实现:
(2)简单选择排序的时间复杂度分析:(简单选择排序的性能上略优于冒泡排序)void select_sort(int arr[],int len) { int i,j; int min; for (i=0;i<len-1;++i) { min = i; for (j=i+1;j<len;++j) { if (arr[min]>arr[j]) { min = j; } } if (min != i) { swap(&arr[min],&arr[i]); } } }
无论最好最差的情况,其比较次数都是一样的多,第i趟排序需要进行n-i次关键字的比较,此时需要比较的n(n-1)/2次。对于交换次数而言,当最好的时候,交换为0次,最差的时候就是初始降序时,交换次数为n-1次,最终的排序时间是比较与交换的次数总和,因此,总的时间复杂度依然为O(n2)。
O(n2)。3、直接插入排序:
(1)直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
代码实现:
(2)直接插入排序的时间复杂度分析:(直接插入排序比冒泡和选择排序的性能要好一些)void insert_sort(int arr[], int len) { for (int i = 1; i < len; ++i) { if (arr[i] < arr[i-1]) { int tmp = arr[i]; int j; for (j = i - 1;j >= 0 && arr[j] > tmp; --j) { arr[j + 1] = arr[j]; } arr[j + 1] = tmp; } } }
当最好的情况,就是要排序的表本身是有序的,比如{2,3,4,5,6},由于没有移动的记录,时间复杂度是O(n),当最坏的情况,即待排序表是逆序的情况,比如{6,5,4,3,2},其时间复杂度是O(n2)。
(3)适用条件:记录本身基本有序;记录数比较少
4、希尔排序
(1)将原有大量记录数的记录进行分组。分割成若干子序列,此时每个子序列待排序的记录个数就比较少,然后在这些子序列内分别进行直接插入排序,当真整个序列都基本有序时,注意只是基本有序时,再对全体记录进行一次直接插入排序。
(2)所谓的基本有序,就是小的关键字基本在前面,大的基本在后面,不大不小的基本在中间,像{2,1,3,6,4,7,5,8,9}这样可以称为基本有序了。
(3)分割待排序记录的目的是减少待排序记录的个数,并使整个序列向基本有序发展。我们采取跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
(4)增量序列的最后一个增量值必须为1。
代码实现:
void shell_sort(int arr[],int len) { int increament = len; int i,j; do { increament = increament/3+1; for (i=increament;i<len;++i) { int temp = arr[i]; if (arr[i]<arr[i-increament]) { for (j=i-increament;j>=0&&arr[j]>temp;j-=increament) { arr[j+increament] = arr[j]; } arr[j+increament] = temp; } } } while (increament>1); }
希尔排序的时间复杂度:O(n2),由于记录是跳跃式的移动,故希尔排序不是稳定的排序算法。
5、堆排序:堆排序是简单选择排序的一种改进。
(1)堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
(2)堆排序:就是利用堆进行排序的方法。它的基本思想是,先将待排序的子序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素进行交换,此时末尾元素的值就是最大值),然后将剩下的n-1个序列重新构造成一个大顶堆,这样就能得到n个元素中的次小值。如此反复执行,便能得到一个有序序列了。(其实就是将大顶堆转换为小顶堆的过程)
(3)所谓的将待排序的序列构造成一个大顶堆,其实就是从下往上、从右到左,将每个非终端结点(非叶结点)当做根结点,将其和其子树调整成大顶堆。
代码实现:
(4)堆排序的时间复杂度:O(nlogn),堆排序是一种不稳定的排序方法。void heap_sort(int arr[],int len) { //先将数组元素调整成大顶堆 int i; for(i=len/2-1;i>=0;--i)//i为非叶子结点的位置 { heap_adjust(arr,i,len); } //交换最后的结点和根结点的值,并将剩余的元素重新调整为大顶堆 for(i=len-1;i>0;--i) { swap(&arr[0],&arr[i]);//交换根结点和最后一个结点 heap_adjust(arr,0,i); } } void heap_adjust(int arr[],int k,int len) { //k为要调整的位置 //将当前的结点当做根结点,判断其与子结点的大小,如果小于就进行交换 int temp = arr[k]; int j = 2*k+1;//k的左孩子 while(j < len) { if (j+1<len && arr[j]<arr[j+1]) { ++j; } if (temp>=arr[j])//判断当前元素和子结点的大小 { break; } // arr[k] = arr[j];//使用赋值调整 swap(&arr[k],&arr[j]);//使用交换调整 k = j; j = 2 * k + 1; } // arr[k] = temp; }
(5)适应情况:由于初始构建堆所需的比较次数较多,因此,它并不适合待排序序列个数较少的情况。
6、归并排序算法
(1)归并排序就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度是1,然后两两归并,得到┌n/2┐(┌x┐表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,...,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。
代码实现:
使用递归实现:
方法1:
void merge_sort(int arr[],int len) { msort(arr,arr,0,len-1); } void msort(int arr[],int brr[],int s,int t) { int m; int crr[20]; if (s == t)//只有一个元素 { brr[s] = arr[s]; } else { m = (s+t)/2;//将arr[s...t]平分为arr[s...m]和arr[m+1...t] msort(arr,crr,s,m);//递归将arr[s...m]归并为有序的crr[s...m] msort(arr,crr,m+1,t);//递归将arr[m+1...t]归并为有序的crr[m+1...t] merge(crr,brr,s,m,t);//将crr[s...m]和crr[m+1...t]归并为brr[s...t] } } void merge(int arr[],int brr[],int i,int m,int n) { int j,k,s; //将所有元素从小到大拷贝 for (j=m+1,k=i;i<=m && j<=n;k++) //循环结束后,一定会结束前半个(或后半个)数组的复制,即i或j会超出范围 { if (arr[i]<arr[j]) { brr[k] = arr[i++]; } else { brr[k] = arr[j++]; } } if(i<=m)//i没有超出 { for(s=0;s<=m-i;s++)//将剩下元素arr[i...m]拷贝到brr { brr[k+s] = arr[i+s]; } } if (j<=n)//j没有超出 { for(s=0;s<=n-j;s++)将剩下元素arr[j...n]拷贝到brr { brr[k+s] = arr[j+s]; } } }
方法2:
void merge_sort2(int arr[],int len) { msort2(arr,0,len-1); } void msort2(int arr[],int start,int end) { if (start < end) { int mid = (start+end)/2; msort2(arr,start,mid); msort2(arr,mid+1,end); merge2(arr,start,mid,end); } } void merge2(int arr[],int start,int mid,int end) { int *brr = (int *)malloc(sizeof(int)*(end-start+1)); int low1,low2,high1,high2; low1 = start; low2 = mid+1; high1 = mid; high2 = end; int i = 0; while (low1 <= high1 && low2 <= high2) { if (arr[low1] < arr[low2]) { brr[i++] = arr[low1++]; } else { brr[i++] = arr[low2++]; } } while (low1 <= high1) { brr[i++] = arr[low1++]; } while (low2 <= high2) { brr[i++] = arr[low2++]; } int j = 0; for (i=start;i<=end;i++) { arr[i] = brr[j++]; } free(brr); }
非递归实现:非递归的迭代做法更加直截了当,从最小的序列开始归并直至完成。不需要像归并的递归算法一样,需要先拆分递归,再归并退出递归。
方法1:方法2:void merge_sort(int arr[],int len) { int* tr = (int*)malloc(sizeof(int)*len); int k=1;//初始是1个记录 while (k<len) { merge_pass(arr,tr,k,len);//将无序数组两两归并入tr k = 2 * k; merge_pass(tr,arr,k,len); k = 2 * k; } } void merge_pass(int sr[],int tr[],int s,int n) { int i = 0; int j; while (i < n-2*s+1)//两两归并,s=1,2,4,... { merge(sr,tr,i,i+s-1,i+2*s-1); i = i+2*s; } if (i<n-s)//归并最后两个子序列,当元素个数是>1的奇数时 { merge(sr,tr,i,i+s-1,n-1); } else//若只剩下单个子序列,将其复制到新的数组中 { for (j=i;j<n;j++) { tr[j] = sr[j]; } } }
void merge_sort2(int arr[],int len) { int k = 1; while (k<len) { merge_pass2(arr,k,len); k = 2 * k; } } void merge_pass2(int arr[],int s,int n) { int i = 0; while (i < n-2*s+1)//两两归并 { merge2(arr,i,i+s-1,i+2*s-1); i = i+2*s; } if (i<n-s)//归并最后两个子序列 { merge2(arr,i,i+s-1,n-1); } }
(2)归并排序的时间复杂度:O(nlogn),空间复杂度:O(n+logn),归并排序是一种稳定的排序方法。
7、快速排序:冒泡排序的升级
(1)快速排序的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
代码实现:
递归实现
方法1:基础
方法2:优化void quick_sort(int arr[],int len) { qsort(arr,0,len-1); } //partition函数要做的就是先选取当中的一个关键字,比如选择第一个关键字,然后想尽办法将它放到一个位置,使得它左边的值都比它小, //右边的值比它大,我们将这样的关键字成为枢轴。 void qsort(int arr[],int low,int high) { int mid; if(low < high) { mid = partition(arr,low,high);//将arr[low...hign]一分为二,mid是枢轴 qsort(arr,low,mid-1);//对低子表递归排序 qsort(arr,mid+1,high);//对高子表递归排序 } } int partition(int arr[],int low,int high) { int tmp = arr[low]; while (low<high) { while (low<high && arr[high]>=tmp) { high--; } swap(&arr[low],&arr[high]); while (low<high && arr[low]<=tmp) { low++; } swap(&arr[low],&arr[high]); } return low; }
void quick_sort(int arr[],int len) { qsort(arr,0,len-1); } //partition函数要做的就是先选取当中的一个关键字,比如选择第一个关键字,然后想尽办法将它放到一个位置,使得它左边的值都比它小, //右边的值比它大,我们将这样的关键字成为枢轴。 void qsort(int arr[],int low,int high) { int mid; while (low < high) { mid = partition(arr,low,high);//将arr[low...hign]一分为二,mid是枢轴 qsort(arr,low,mid-1);//对低子表递归排序 low = mid+1;//优化,将递归深度降低 } } int partition(int arr[],int low,int high) { int tmp = arr[low]; while (low<high) { while (low<high && arr[high]>=tmp) { high--; } arr[low] = arr[high];//优化,将交换改成赋值 while (low<high && arr[low]<=tmp) { low++; } arr[high] = arr[low];//优化,将交换改成赋值 } arr[low] = tmp; return low; }
(2)快速排序的时间复杂度分析:在最优的的情况下,快速排序算法的时间复杂度是O(nlogn)。在最坏情况下,时间复杂度是O(n2)。空间复杂度:O(logn),快速排序是一种不稳定的排序方法。 (关键字的比较和交换是跳跃进行的)
(3)快速排序的优化:
a.优化选取枢轴
三数取中法:取三个关键字先进行排序,将中间数作为枢轴,一般是取左端、右端和中间三个数,也可以随机选取。
在partition函数中添加代码是arr[low]是三个数中的中间值。
int m = low + (hign - low) / 2;//计算数组中间的元素的下标 if(arr[low] > arr[hign]) swap(&arr[low],&arr[hign]);//交换左端和右端数据,保证左端较小 if(arr[m] > arr[hign]) swap(&arr[m],&arr[hign]);//交换中间与右端数据,保证中间较小 if(arr[m] > arr[low]) swap(&arr[low],&arr[m]);//交换中间和左端数据,保证左端较大 //此时arr[low]是整个序列左中右三个关键字的中间值
b.优化不必要的交换
c.优化小数组时的排序方案:如果数组非常小,其实快速排序反而不如直接插入排序来得更好(直接插入是简单排序中性能最好的)。
d.优化递归操作:栈的大小是很有限的,每次递归调用都会耗费一定的技空间,函数的参数越多,每次递归耗费的空间也越多。因此如果能减少递归,将会大大提高性能。
非递归快速排序:
void qsort2(int arr[],int low,int high) { stack<int> st; if(low < high) { int mid = partition(arr,low,high); if(low < mid-1) { st.push(low); st.push(mid-1); } if(mid+1 < high) { st.push(mid+1); st.push(high); } //其实就是用栈保存每一个待排序子串的首尾元素下标, //下一次while循环时取出这个范围,对这段子序列进行partition操作 while(!st.empty()) { int q = st.top();//待排序子串的尾元素下标 st.pop(); int p = st.top();//待排序子串的首元素下标 st.pop(); mid = partition(arr,p,q); if(p < mid-1) { st.push(p); st.push(mid-1); } if(mid+1 < q) { st.push(mid+1); st.push(q); } } } } void qsort3(int arr[],int low,int high) { stack<int> sta; if (low < high) { sta.push(high); sta.push(low); while (!sta.empty()) { int l = sta.top(); sta.pop(); int r = sta.top(); sta.pop(); int mid = partition(arr,l,r); if (l < mid - 1) { sta.push(mid - 1); sta.push(l); } if(mid+1 < r) { sta.push(r); sta.push(mid+1); } } } } void quick_sort2(int arr[],int len) { qsort2(arr,0,len-1); //qsort3(arr,0,len-1); }
算法比较
从算法的简单性来看,我们将以下7种算法分为两类:
简单算法:冒泡排序、简单选择排序、直接插入排序。
改进算法:希尔排序、堆排序、归并排序、快速排序。
从平均情况来看,显然最后3 种改进算法要胜过希尔排序,并远远胜过前3 种简
单算法。
从最好情况看,反而冒泡和直接插入排序要更胜一筹,也就是说,如果你的待排
序序列总是基本有序,反而不应该考虑4种复杂的改进算法。
从最坏情况看,堆排序与归并排序叉强过快速排序以及其他简单排序。
从空间复杂度来说,归并排序强调要马跑得快,就得给马吃个饱。快速排序也有
相应的空间要求,反而堆排序等却都是少量索取,大量付出,对空间要求是O(1)。如果执行算法的软件所处的环境非常在乎内存使用量的多少时,选择归并排序和快速排
序就不是一个较好的决策了。
从稳定性来看,归并排序独占整头,我们前面也说过,对于非常在乎排序稳定性
的应用中,归并排序是个好算法。从待排序记录的个数上来说,待排序的个数n越小,采用简单排序方法越合适。
反之,n 越大,采用改进排序方法越合适。从综合各项指标来说,经过优化的快速排序是性能最好的排序算法。
常用的内排序算法的描述和实现
最新推荐文章于 2022-09-17 23:51:18 发布