假定一数组 arr[] = { 9,8,7,6,5,4,3,2,1 } 需改为升序排列。
一、插入排序
我们可以将这个数组分为两个部分--已排序的部分和未排序的部分,而第一个元素是已经排好的。
然后再用第一个未被排序的元素n与已经排序的最后一个元素m进行比较,如果n<m,就与m的前一个数进行比较,同时将这些元素向后移,直到遇到一个小于n的数或是达到了下标为0的位置,再将这个数存下。
之后重复这个过程直到遍历完这个数组。
时间复杂度 | O(N^2) |
空间复杂度 | O(1) |
稳定性 | 稳定 |
元素集合越接近有序,插入排序的时间效率越高。
void InsertSort(int*arr,int n)
{
for (int i = 0; i < n-1; i++)
{
int end = i;
int tmp = arr[i + 1];
while(end>=0)
{
if (tmp < arr[end])
{
arr[end + 1] = arr[end];
end--;
}
else
break;
}
arr[end + 1] = tmp;
}
}
二 、希尔排序
因为直接排序的元素集合越接近有序,插入排序的时间效率越高,因此希尔排序就是先让集合尽可能的接近有序,再进行一个插入排序。
我们还是将数组 arr[] = { 9,8,7,6,5,4,3,2,1 } 改为升序排列。
首先确定一个gap的值(这里取gap=3),让每隔gap-1的数进行比较并进行交换来进行一个预排序。这时再进行插入排序,就会快的多了。
但是如果数组中的较多的时候,比如数组中含有1亿个数,那么只交换一次再进行插入排序,并不能比插入排序快多少,因此我们需要调整gap的值。
因为gap的值越大,数组中大的值越快排到后面,小的数越快到前面,但是不接近有序。而gap的值越小,越接近有序,当gap等于1时,就是插入排序。因此我们可以取gap等于数组长度n或n/2等,并且在预排序时逐渐的减小gap的值,直到gap=1,就可以完成集合的排序。
时间复杂度 | |
稳定性 | 不稳定 |
相同的值在预排时有可能分到不同组里,所以不稳定。
void ShellSort(int*arr,int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;//防止gap为0,且最后一次执行时gap一定为1
for (int i = 0; i < n - gap; i++)//最后gap个会在end+gap时被比较
{
int end = i;
int tmp = arr[i + gap];
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + gap] = arr[end];
end=end-gap;
}
else
break;
}
arr[end + gap] = tmp;
}
}
}
三、堆排序
四、选择排序
我们每遍历一遍数组就能找到数组中的最大值和最小值,我们再将这两个值放在头和尾就完成了一次排序,之后再次遍历剩下的数组,重复这个过程直到完成排序。
时间复杂度 | |
空间复杂度 | O(1) |
稳定性 | 不稳定 |
选择排序是不稳定的!
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
while (left < right)
{
int maxi = left;
int mini = left;
for (int i = left+1; i <= right; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
swap(&a[mini], &a[left]);
if (left == maxi)//此时maxi为0
{
maxi = mini;//防止left和mini相等时,maxi被换完后又被换回去
}
swap(&a[maxi], &a[right]);
left++;
right--;
}
}
五、冒泡排序
老熟人冒泡排序想必大家都很熟悉,就是通过每个数的相互比较,使每个数找到正确的位置,从而完成排序。
时间复杂度 | |
空间复杂度 | O(1) |
稳定性 | 稳定 |
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n; i++)
{
int exchange = 0;
for (int j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j+1])
{
Swap(&a[j+1], &a[j]);
exchange = 1;
}
}
if (exchange == 0)//如果为0表示没有进行过交换,即已经进行完排序
break;
}
}
六、快速排序
1.hoare版
这里我们用arr[]={5,3,2,6,7,1,9,8,4}这个数组。
我们首先选定一个值key进行排序,从左右两侧寻找比key小和比key大的数并交换,最后再将key与left和right相交时的点交换,使key左侧的值都比key要小,右侧的值都比key大。
之后再通过递归对key两侧的数进行排序。
void PartSort1(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int left = begin;
int right = end;
int keyi = left;//keyi为下标
while (left < right)
{
while (left < right && a[right]>=a[keyi])//让右先走,保证相遇的位置比key对应的数要小
//要是相遇的位置比key对应数要大的话,交换完后key的左侧就不全是比key对应数要小的数了
{
right--;
}
while (left < right && a[left] <= a[keyi])
{
left++;
}
swap(&a[left], &a[right]);
}
swap(&a[left], &a[keyi]);
keyi = left;
PartSort1( a, begin, keyi - 1);
PartSort1(a, keyi+1, end);
}
2.挖坑法
我们还是先选取一个key值,在从右向左找比key要小的值,不过这次需要把找到的值赋给key所在的位置。这样我们可以理解为把a[right]的值赋给a[keyi]后,a[right]被挖去。
之后再从左寻找比key要大的值 ,将a[left]值赋给a[right],同时再将a[left]挖去。最后将key的值放在a[left]位置上,重复这个过程直到left=right,再将key的值放于此处。
之后再通过递归对left(right)两侧进行排序,使得各元素达到正确位置。
void PartSort2(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int left = begin;
int right = end;
int key = a[left];//需要保存被挖掉的值
while (left < right)
{
while (left < right && a[right] >= key)
{
right--;
}
a[left] = a[right];
//找到一个比key(a[left])要小的值,并把这个值放在a[left]的位置
while (left < right && a[left] <= key)
{
left++;
}
a[right] = a[left];
//把找到的值放在刚才a[right]的空位上
}
a[left] = key;
PartSort2(a, begin, left-1);
PartSort2(a, left + 1, end);
}
3.前后指针法
确立key后,使prev=left,cur=left+1,之后++cur直到遇到比key小的值再++prev,交换a[prev]与a[cur]的值,遍历完后再交换a[prev]和a[keyi]的值从而完成一次排序。之后再递归排完key两侧的数。
4.非递归方式(栈实现)
通过观察第一种方法我们可以发现,每进行一次排序就会使key两侧的数变成都比key大或是小的数,之后就是通过递归来重复这个过程,因此我们只需要递归的实现即可。每一次递归的参数都是key一侧的头和尾,所以我们可以用栈来保存这两个值,需要递归时就取出栈顶的两个值,这两个值就可以作为参数完成一次排序。
int PartSort(int* a, int begin, int end)
{
int left = begin;
int right = end;
int key = a[left];
while (left < right)
{
while (left < right && a[right] >= key)
{
right--;
}
a[left] = a[right];
while (left < right && a[left] <= key)
{
left++;
}
a[right] = a[left];
}
a[left] = key;
return left;
}
void QuickSortNonR(int* a, int begin, int end)
{
Stack st;
StackInit(&st);
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))
{
int left;
int right;
right = StackTop(&st);
StackPop(&st);
left = StackTop(&st);
StackPop(&st);
int keyi = PartSort(a, left, right);//进行一次排序
if (left < keyi-1)
{
StackPush(&st, left);
StackPush(&st, keyi-1);
}
if (keyi+1 < right)
{
StackPush(&st, keyi + 1);
StackPush(&st, right);
}
}
}
5.优化
假设需要排的数组是arr={1,2,3,4,5,6,7,8,9},这样一个已经被排好的数组,就会使时间复杂度达到了。 因此我们不能一直将最左侧或最右侧的值作为key,而是应该根据不同的情况更改key值。
(1)三数取中
取最左值、最右值和中间数三者中不是最大也不是最小的值,然后再将它与最左侧或最右侧的值进行交换。
int GetMidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 2;//防止left+right的值超出int大小
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
(2) 小区间插入
如果数组的子区间较小,可以采用插入排序的方法,因此在排序前可以判断一下。
void QuickSort2(int* a, int begin, int end)
{
if (begin >= end)
return;
if (end - begin + 1 <= 10)
{
InsertSort(a + begin, end - begin + 1);
}
else
{
int keyi = PartSort2(a, begin, end);
QuickSort2(a, begin, keyi - 1);
QuickSort2(a, keyi + 1, end);
}
}
时间复杂度 | O(N*logN) |
空间复杂度 | O(logN) |
稳定性 | 不稳定 |
七、归并排序
1.递归方法
归并排序是通过比较begin1和begin2所指对象的大小来完成排序的,但是如果begin1和begin2的数组并不有序排出来的数组也不是有序的,所以可以将其分治成小区间来进行比较完成,从而完成小区间内的排序。
我们再将较小值尾插入一个临时数组,完成排序时再将临时数组的值赋值给原数组。
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
return;
int mid = (left+right) / 2;
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int i = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
tmp[i++] = a[begin2++];
}
while (begin1 <= end1)//把没走完的剩下部分存入
tmp[i++] = a[begin1++];
while (begin2 <= end2)
tmp[i++] = a[begin2++];
for (int j = left; j <= right; j++)//把排好的数放回去
{
a[j] = tmp[j];
}
}
void MergeSort(int* a, int n)
{
int*tmp = (int*)malloc(sizeof(int) * n);
_MergeSort(a,0,n-1,tmp);
free(tmp);
}
2.非递归方法
void _MergeSortNonR(int* a, int begin1, int end1,int begin2,int end2, int* tmp)
{
int j = begin1;//保存begin1的值,防止被更改后找不到
int i = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
tmp[i++] = a[begin2++];
}
while (begin1 <= end1)
tmp[i++] = a[begin1++];
while (begin2 <= end2)
tmp[i++] = a[begin2++];
for (j; j <= end2; j++)
{
a[j] = tmp[j];
}
}
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i = 2*gap+i)
{
int begin1 = i, end1 = i + gap - 1, begin2 = i + gap, end2 = i + 2 * gap - 1;
if (begin2 >= n)//最后一个小组的第一个小区间不够gap个就跳出,在更大的区间来排
break;
if (end2 >= n)//最后一个小组的最后一个小区间不够gap或是根本没有,就不需要归并了
end2 = n - 1;
_MergeSortNonR(a,begin1,end1,begin2,end2,tmp);
}
gap *= 2;
}
free(tmp);
}
时间复杂度 | O(N*logN) |
空间复杂度 | O(N) |
稳定性 | 稳定 |
八、非比较排序
首先创建一个size大小的空间,size等于最大值减最小值。每有一个相同的数字就使该数字减去最小值作为下标,使其加1。然后根据创建数组中的值重新赋值给原数组即可完成排序。
void CountSort(int* a,int n)
{
int max = a[0];
int min = a[0];
for (int i = 1; i < n; i++)
{
if (max < a[i])
max = a[i];
if (min > a[i])
min = a[i];
}
int range = max - min + 1;
int* count = (int*)calloc(n, sizeof(int) * range);
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
int i = 0;
for (int j = 0; j < range; j++)
{
while (count[j]--)
{
a[i++] = j + min;
}
}
}
局限性:只适合一组数,只适合整数,且数据越集中效率越高。