基础知识介绍
1、稳定排序和非稳定排序
简单地说就是所有相等的数经过某种排序方法后,仍能保持它们在排序之前的相对次序,我们就说这种排序方法是稳定的。反之,就是非稳定的。比如:一组数排序前是a1,a2,a3,a4,a5,其中a2=a4,经过某种排序后为a1,a2,a4,a3,a5,则我们说这种排序是稳定的,因为a2排序前在a4的前面,排序后它还是在a4的前面。假如变成a1,a4,a2,a3,a5就不是稳定的了。
2、内排序和外排序
在排序过程中,所有需要排序的数都在内存,并在内存中调整它们的存储顺序,称为内排序;
在排序过程中,只有部分数被调入内存,并借助内存调整数在外存中的存放顺序排序方法称为外排序。
3、算法的时间复杂度和空间复杂度
所谓算法的时间复杂度,是指执行算法所需要的计算工作量。
一个算法的空间复杂度,一般是指执行这个算法所需要的内存空间。
插入排序
1、直接插入排序(Straight Insertion Sort)
思想:整个记录分为有序区和无序区,依次从无序区取一个记录插入到有序区。下标为0的记录设置为哨兵,用作比较。
核心代码:
for(int i = 2; i <= n; i++){
iArray[0] = iArray[i];
int j;
for(j = i - 1; iArray[j] > iArray[0]; j--){
iArray[j+1] = iArray[j];
}
iArray[j+1] = iArray[0];
}
时间复杂度:
(1)最好情况:整个序列为正序,总的比较次数是n-1,移动次数是2(n-1),因此时间复杂度是O(n);
(2)最坏情况:真个序列为逆序,在第i趟插入时,i需与临界条件以及前面i-1个记录做比较,因此总的比较次数是2+.....+n,总的比较次数是从(n+2)*(n - 1)/2,同时移动次数是3+......+(n+1),总的移动次数是(n+4)*(n-1)/2,因此时间复杂度是O(n^2)。
(3)平均情况下,各种排列的概率相同,在插入第i个记录时平均所需比较的记录时序列的一半,所以总的比较次数是(n*2)(n-1)/4,移动次数是(n+4)(n-1)/4,因此,时间复杂度是O(n^2)。
空间复杂度:O(1)。
直接插入排序是一种稳定的排序方法。
2、希尔排序(Shell Sort)
思想:它是对直接插入排序的一种改进,因为当序列基本有序或者记录数较少时,直接插入排序的效率很高。因此希尔排序是将序列分成若干子序列,在子序列内分别进行排序,待整个序列基本有序时,再对整个序列进行直接插入排序。
核心代码:
int d, i, j;
for(d = n / 2; d >= 1; d = d / 2){
//操作与插入排序类似
for(i = d + 1; i <= n; i++){
iArray[0] = iArray[i];
for(j = i -d; j >= 1 && iArray[j] > iArray[0]; j -= d){
iArray[j+d] = iArray[j];
}
iArray[j+d] = iArray[0];
}
}
时间复杂度:分析复杂,据前人大量的实验结果得出,时间性能在
O(n^2)和O(nlog2(n))之间。当n在某个特定范围时,时间性能约为O(n^1.3)。
空间复杂度:O(1)。
希尔排序是一种不稳定的排序方法。
交换排序
1、冒泡排序(Bubble Sort)
思想:序列分为有序区和无序区,在无序区将记录两两比较,如果反序则交换记录,选出的最大记录放在有序区,有序区记录加1,无序区记录减一,循环此过程,直到无序区没有反序为止。
核心代码:
int i, j, exchagne;
exchange = n;
while(exchange != 0){
int flag = exchange;
exchangne = 0;
//比较区间为[1,flag]
for(j = 1; j < flag; j++){
if(iArray[j] > iArray[j+1]){
iArray[0] = iArray[j];
iArray[j] = iArray[j+1];
iArray[j+1] = iArray[0];
//记录上一趟排序最后的交换位置,避免无用比较
exchange = j;
}
}
}
时间复杂度:
(1)最好情况下,整个序列为正序,算法只执行一趟,比较了n-1次,不移动记录。时间复杂度是O(n)。
(2)最坏情况下,整个序列是逆序,总的比较次数是(n-1)+.....+1,因此时间复杂度是(n-1)*n/2,移动次数3*n*(n-1)/2,因此总的时间复杂度是O(n^2)。
(3)平均情况下,时间复杂度与最坏程度同一等级,O(n^2)。
空间复杂度:O(1)。
冒泡排序是一种稳定的排序算法。
2、快速排序(Quick Sort)
思想:冒泡排序记录的比较和移动是在相邻位置进行的,记录每次只能后移一位,因此总的比较次数和移动次数较多。快速排序,比较和移动是从两端向中间进行的,记录的移动距离较远,减少了总的比较次数和移动次数。它是通过选择一个中间轴值,将序列分为两个独立的部分,左侧均小于或等于轴值,右侧均大于或等于轴值。 核心代码:
int getLocate(int *iArray, int first, int end){
int i = first, j = end;
while(i < j){
//循环保证i指针永远小于j指针
while(i<j && iArray[i] <= iArray[j]){
j--;
}
if(i < j){
iArray[0] = iArray[i];
iArray[i] = iArray[j];
iArray[j] = iArray[0];
i++;
}
while(i<j && iArray[i] <= iArray[j]){
i++;
}
if(i < j){
iArray[0] = iArray[i];
iArray[i] = iArray[j];
iArray[j] = iArray[0];
j--;
}
}
return i;
}
void quickSort(int *iArray, int first, int end){
if(first < end){
//获取轴值
int location = getLocate(iArray, first, end);
//递归对每个子序列进行交换排序
quickSort(iArray, first, location-1);
quickSort(iArray, location+1, end);
}
}
时间复杂度:
(1)最好情况:O(nlog2(n))。
(2)最换情况:O(n^2)。
(3)平均情况:O(nlog2(n))。
空间复杂度:递归使用栈,最好情况下栈的深度为O(log2(n)),最坏情况下为O(n),平均情况下为O(log2(n))。
快速排序时一种不稳定的排序方法。是目前公认的一种比较好的排序算法,是qsort的内部实现。
选择排序
1、简单选择排序
思想:将序列分为有序区和无序区,在无序区中依次比较,记录最大记录的下标,比较完毕后,将最大记录放入有序区,依次循环。
核心代码:
int i, j, index;
for(i = 1; i <= n; i++){
//index记录最小记录的下标
index = i;
for(j = i + 1; j <= n; j++){
if(iArray[j] < iArray[index]){
index = j;
}
}
if(index != i){
iArray[0] = iArray[i];
iArray[i] = iArray[index];
iArray[index] = iArray[0];
}
}
时间复杂度:无论序列如何排序,关键码的比较次数都一样,均为(n -1)*n/2,即
O(n^2)。这是简单选择排序的最差、最好、平均时间性能。
空间复杂度:O(1)。
简单选择排序是一种不稳定的排序方法。
2、堆排序(Heap Sort)
思想:简单选择排序每趟排序都是选出最小记录,没有将之前的比较结果保存起来,因此比较次数较多。堆排序是在选择最小关键码的同时,记录较小关键码。堆排序首先将序列构造成一个堆,此时,选出最大的记录,即堆顶。然后移走堆顶,再将堆调整为大根堆,进行循环,直到堆中只有一个记录为止。
核心代码:
void adjustHeap(int *iArray, int locate, int n){
int i = locate, j = 2 * locate;
while(j <= n){
if(j < n && iArray[j] < iArray[j+1]){
j++;
}
if(iArray[i] < iArray[j]){
iArray[0] = iArray[j];
iArray[j] = iArray[i];
iArray[i] = iArray[0];
i = j;
j = 2 * i;
}else{
break;
}
}
}
void heapSort(int *iArray, int n){
int i, j;
for(i = n /2; i >= 1; i--){
adjustHeap(iArray, i, n);
}
for(j = n; j > 1; j--){
iArray[0] = iArray[j];
iArray[j] = iArray[1];
iArray[1] = iArray[0];
adjustHeap(iArray, 1, j-1);
}
}
时间复杂度:初建堆需要O(n)时间,第i次取堆顶记录重建堆需要O(log2(i))时间,并且需要取n-1次堆顶记录。因此总的时间复杂度为
O(nlog2(n)),这是堆排序的最好、最差、平均情况。
空间复杂度:O(1)。
堆排序时一种不稳定的排序方法。
归并排序
1、二路归并排序算法(非递归算法)
思想:将若干有序序列两两归并,直至所有待排序记录都在一个有序序列为止。
核心代码:
//一次归并
void merge(int *iArray, int *iArray1, int startLocate, int endLocate1, int endLocate2){
int k = startLocate;
int i = k, j = endLocate1 + 1;
int m1 = endLocate1, m2 = endLocate2;
//m1:子序列1的结束位置 m2:子序列2的结束位置
while(i <= m1 && j <= m2){
if(iArray[i] <= iArray[j]){
iArray1[k++] = iArray[i++];
}else{
iArray1[k++] = iArray[j++];
}
}
if(i <= m1){
while(i <= m1){
iArray1[k++] = iArray[i++];
}
}else{
while(j <= m2){
iArray1[k++] = iArray[j++];
}
}
}
/*
归并公共存在三种关系:
1、当i<n-2*h+1时,存在两个长度相同的子序列,可以进行归并操作
2、当i<n-h+1时,存在两个子序列,当其中一个子序列长度不足h,可以进行归并操作。
3、当i>=n-h+1时,只有一个子序列。直接复制到数组末尾。
*/
void mergePass(int *iArray, int *iArray1, int distance, int len){
int i = 1;
int n = len;
int h = distance;
while(i <= n-2*h+1){
merge(iArray, iArray1, i, i+h-1, i+2*h-1);
i += 2*h;
}
if(i < n-h+1){
merge(iArray, iArray1, i, i+h-1, n);
}else{
for(int k = i; k <= n; k++){
iArray1[k] = iArray[k];
}
}
}
//定义一个长度为n的数组用来存放归并结果
void mergeSort(int *iArray, int *iArray1, int n){
int h = 1;
//保证成对操作,防止当归并趟数为奇数时,结果保存在iArray1中
while(h < n){
mergePass(iArray, iArray1, h, n);
h = 2*h;
// 查看h值 cout<<h<<endl;
mergePass(iArray1, iArray, h, n);
h = 2*h;
// 查看h值 cout<<h<<endl;
}
}
时间复杂度:一趟归并所需时间为n,整个归并排序时间复杂度是O(log2(n))。因此,总的时间复杂度是
O(nlog2(n)),这是归并排序算法最好、最坏、平均的时间性能。
空间复杂度:O(n)。
二路归并时一种稳定的排序方法。
排序方法比较