上篇文章我们说到了堆排序,那么接下来我们将冒泡,快排和归并说完。
交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
代码如下
//冒泡排序
void BubbleSort(int* a, int n)
{
int exchange = 0;
for (int j = 0; j < n; j++)
{
for (int i = 1; i < n - j; i++)
{
if (a[i - 1] > a[i])
{
swap(&a[i - 1], &a[i]);
exchange = 1;
}
/*一趟排序如果没有交换,则说明是有序的*/
if (exchange == 0)
{
break;
}
}
}
}
冒泡排序的特性总结:
1. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定
快速排序
分为三个版本
Hoare版本
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
// 当最坏情况,数组接近有序,递归会非常麻烦
// 三数取中:三个数中取不大不小的那个
int GetMidIndex(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] > a[mid])
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[begin] > a[end])
{
return end;
}
else
{
return begin;
}
}
else //begin < mid
{
if (a[mid] < a[end])
{
return mid;
}
//mid > end
else if (a[begin] > a[end])
{
return begin;
}
else
return end;
}
}
//Hoare
int PartSort1(int* a, int begin, int end)
{
// 三数取中:三个数中取不大不小的那个
int mid = GetMidIndex(a, begin, end);
swap(&a[begin], &a[mid]);
int left = begin, right = end;
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左边开始
while (left < right && a[left] <= a[keyi])
{
++left;
}
//交换
swap(&a[left], &a[right]);
}
//相遇再交换
swap(&a[left], &a[keyi]);
keyi = left;
return left;
}
void QuickSort(int* a, int begin,int end)
{
if (begin >= end)
return;
//if ((end - begin + 1) < 15)
//{
// //小区间优化
// InsertSort(a + begin, end - begin + 1);
//}
else
{
int keyi = PartSort1(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
}
优化
最坏情况下,每次选key都是最大或最小的,时间复杂度为O(N^2)
当加入三数取中后会从最坏的变为最好的,几乎不会出现最坏的情况。 时间复杂度为O(N*logN)
递归到最后面时,会出现浪费一半的时间,所以我们通常会在最后10个或者15个来采用直接插入排序来节省时间。
挖坑法
// 当最坏情况,数组接近有序,递归会非常麻烦
// 三数取中:三个数中取不大不小的那个
int GetMidIndex(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] > a[mid])
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[begin] > a[end])
{
return end;
}
else
{
return begin;
}
}
else //begin < mid
{
if (a[mid] < a[end])
{
return mid;
}
//mid > end
else if (a[begin] > a[end])
{
return begin;
}
else
return end;
}
}
void QuickSort(int* a, int begin,int end)
{
if (begin >= end)
return;
//if ((end - begin + 1) < 15)
//{
// //小区间优化
// InsertSort(a + begin, end - begin + 1);
//}
else
{
int keyi = PartSort1(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
}
//挖坑法
int PartSort2(int* a, int begin, int end)
{
int mid = GetMidIndex(a, begin, end);
swap(&a[begin], &a[mid]);
int left = begin, right = end;
int keyi = a[left];
int hole = left;
while (left < right)
{
//左边开始
while (left < right && a[right] >= a[keyi])
{
right--;
}
a[hole] = a[right];
hole = right;
while (left < right && a[left] <= a[keyi])
{
++left;
}
a[hole] = a[left];
hole = left;
}
a[hole] = keyi;
return hole;
}
先将第一个变量存入key中(通过三数取中),从右边开始走小于key将它的值放入key的坑位上,该地点重新变为坑位,然后左边开始走,找到比key大的数,放到坑位上去。
双指针法
这个是比较简单的一种快排,但是不好去理解。
先上代码
// 当最坏情况,数组接近有序,递归会非常麻烦
// 三数取中:三个数中取不大不小的那个
int GetMidIndex(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] > a[mid])
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[begin] > a[end])
{
return end;
}
else
{
return begin;
}
}
else //begin < mid
{
if (a[mid] < a[end])
{
return mid;
}
//mid > end
else if (a[begin] > a[end])
{
return begin;
}
else
return end;
}
}
int PartSort3(int* a, int begin, int end)
{
int mid = GetMidIndex(a, begin, end);
swap(&a[begin], &a[mid]);
int prev = begin, cur = begin + 1;
int keyi = begin;
while (cur <= end)
{
if (a[cur] < a[keyi])
{
swap(&a[++prev], &a[cur]);
}
++cur;
}
swap(&a[keyi], &a[prev]);
keyi = prev;
return keyi;
}
//时间复杂度O(NlogN)
//空间O(logN)
//快速排序
void QuickSort(int* a, int begin,int end)
{
if (begin >= end)
return;
//if ((end - begin + 1) < 15)
//{
// //小区间优化
// InsertSort(a + begin, end - begin + 1);
//}
else
{
int keyi = PartSort1(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
}
这个就是cur先走找到比key小的然后和prev交换,刚开始他们一样所以交换看不出来,后来就会拉开差距。
快速排序的特性总结:
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定
归并排序
归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
//时间复杂度O(NlogN)
//空间O(N)
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
{
return;
}
int mid = (begin + end) / 2;
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1,end, tmp);
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
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++];
}
memcpy(a + begin, tmp + begin, sizeof(int*) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return -1;
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
归并排序就是一分为二,二分为四,四分为八的比较交换。
归并排序的特性总结:
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定