大家好我是沐曦希💕
书接【数据结构初阶】八大排序(一)——希尔排序&&堆排序&&直接插入排序&&直接选择排序
1.交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
1.1 冒泡排序
void BubbleSort(int* a, int n)
{
int flag = 1;
int i = 0;
for (i = 0; i < n; i++)
{
flag = 1;
for (int j = 0; j < n - 1 - i; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
flag = 0;
}
}
if (flag == 1)
break;
}
}
int main()
{
int a[] = { 11,45,33,18,36,41,39,35,21,31,17,10,28 };
BubbleSort(a, sizeof(a) / sizeof(a[0]));
PrintArray(a, sizeof(a) / sizeof(a[0]));
return 0;
}
冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
排序性能对比
void testOp()
{
srand((unsigned int)time(0));
const int N = 100000;
int* a1 = (int*)malloc(sizeof(int) * N);
assert(a1);
int* a2 = (int*)malloc(sizeof(int) * N);
assert(a2);
int* a3 = (int*)malloc(sizeof(int) * N);
assert(a3);
int* a4 = (int*)malloc(sizeof(int) * N);
assert(a4);
int* a5 = (int*)malloc(sizeof(int) * N);
assert(a5);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
}
int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
int begin3 = clock();
SelectSort(a3, N);
int end3 = clock();
int begin4 = clock();
HeapSort(a4, N);
int end4 = clock();
int begin5 = clock();
BubbleSort(a5, N);
int end5 = clock();
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
printf("BubbleSort:%d\n", end5 - begin5);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
a1 = NULL;
a2 = NULL;
a3 = NULL;
a4 = NULL;
a5 = NULL;
}
当N为10000时.
1.2 快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
void QuickSort(int array[], int left, int right)
{
if(right - left <= 1)
return;
int div = partion(array, left, right);
QuickSort(array, left, div);
QuickSort(array, div+1, right);
}
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。 将区间按照基准值划分为左右两半部分的常见方式有:
单躺排序:1.选一个key。(一般是第一个或者是最厚一个) 2.单躺排序,要求小的在key的左边,大在key的右边。 以第一个为key: left找比key小的,right找比key大的,right先走,当right和left都停下来变交换。相遇后将left(right)所指的值与key所指的值交换。 那么如何保证相遇位置比key小(左边第一个左key):rgiht先走。 相遇两种情况:1.right停下来,left遇到R相遇,相遇位置比key小。 2.left停下来,right遇到left,相遇位置比key小。 同样道理:如果右边第一个为key:left先走找大的,right在left停下来后走找小的。
1.2.1 hoare版本
具体思路
具体思路是: 选定一个基准值,最好选定最左边或者最右边,选中间会给自己找麻烦。 确定两个指针left 和right 分别从左边和右边向中间遍历数组。 如果选最左边为基准值,那么right指针先走,如果遇到小于基准值的数就停下来。 然后左边的指针再走,遇到大于基准值的数就停下来。 交换left和right指针对应位置的值。 重复以上步骤,直到left = right ,最后将基准值与left(right)位置的值交换。 这样基准值左边的所有数都比他小,而他右边的数都比他大,从而他所在的位置就是排序后的正确位置。
之后再递归排以基准值为界限的左右两个区间中的数,当区间中没有元素时,排序完成。
动图演示 这里选择右边是基准值:
单趟演示: 重复以上步骤直到左右指针相遇: 此时就把序列分为6之前的左序列和右序列,再用递归分别对左序列和右序列进行排序,每次排好一个数字位置。
代码
int PartSort1(int* a, int left,int right)
{
int keyi = left;
int begin = left;
int end = right;
while (left < right)
{
while (a[right] >= a[keyi] && right > left)
{
--right;
}
while (a[left] <= a[keyi] && right > left)
{
++left;
}
if(left < right)
Swap(&a[right], &a[left]);
}
Swap(&a[left], &a[keyi]);
return left;
}
void QuickSort(int* a, int left, int right)
{
int keyi = PartSort1a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
int main()
{
int a[] = { 11,45,33,18,36,41,39,35,21,31,17,10,28 };
QuickSort(a, 0, sizeof(a) / sizeof(a[0]) - 1);
PrintArray(a, sizeof(a) / sizeof(a[0]));
return 0;
}
1.2.2 挖坑法
挖坑法与上面的方法类似。
具体思路
先将选定的基准值(最左边)直接取出,然后留下一个坑。 当右指针遇到小于基准值的数时,直接将该值放入坑中,而右指针指向的位置形成新的坑位, 然后左指针遇到大于基准值的数时,将该值放入坑中,左指针指向的位置形成坑位, 重复该步骤,直到左右指针相等。最后将基准值放入坑位之中。 之后也是以基准值为界限,递归排序基准值左右区间。
单趟演示:
重复以上步骤直到左右指针相遇: 此时就把序列分为6之前的左序列和右序列,再用递归分别对左序列和右序列进行排序,每次排好一个数字位置。
代码
int PartSort2(int* a, int left, int right)
{
int key = a[left];
int hole = left;
while (left < right)
{
while (left < right && a[right] >= key)
--right;
a[hole] = a[right];
hole = right;
while (left < right && a[left] <= key)
++left;
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
1.2.3 前后指针版本
前后指针法是一个新思路,不太好理解,但是代码比较简单。
具体思路
选定基准值,定义prev和cur指针(cur = prev + 1) prev始终要在大于key的值前面一个位置。 cur先走,遇到小于基准值的数停下,然后将prev向后移动一个位置 将prev对应值与cur对应值交换 重复上面的步骤,直到cur走出数组范围 最后将基准值与prev对应位置交换 递归排序以基准值为界限的左右区间
单趟演示:
代码
int PartSort3(int* a, int left, int right)
{
int prev = left;
int cur = left + 1;
int keyi = left;
while (cur <= right)
{
while (a[cur] <= a[keyi] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
++cur;
}
Swap(&a[keyi], &a[left]);
return left;
}
1.3 快速排序的优化
上面就是快速排序递归的三种方法。 但是上面的程序还有一些缺陷: 1.在基准值的选择上,如果选择的基准值为恰好为最小值,会进行不必要的递归。 2.在排序大量有序数据或者接近有序数据时,效率会比较低,甚至可能会出现程序崩溃的情况。这是因为在排序有序数据时,快速排序的递归调用次数过多,会导致栈溢出的情况。
为了解决这些问题,这里有两种优化方法:
1.3.1 三数取中法选基准值
递归到小的子区间时,可以考虑使用插入排序 1.即在在起始位置,中间位置,末尾位置中选出中间值,作为基准值。
int getmidindex(int* a,int left,int right)
{
int mid = left + (right - left) / 2;
if (a[mid] > a[left])
{
if (a[mid] < a[right])
return mid;
else if (a[left] > a[right])
return left;
else
return right;
}
else
{
if (a[mid] > a[right])
return mid;
else if (a[left] < a[right])
return left;
else
return right;
}
}
1.3.2 小区间优化
类似于二叉树,每个子树都会进行一次递归调用,越到下面递归调用会越多。为了减少递归调用,当到递归到下层时,我们可以使用其他的排序来替代。这里我们使用插入排序。
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
if (right - left <= 8)
{
InsertSort(a + left, right - left + 1);
}
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
1.3.3 完整代码
以挖坑法为例:
int getmidindex(int* a,int left,int right)
{
int mid = left + (right - left) / 2;
if (a[mid] > a[left])
{
if (a[mid] < a[right])
return mid;
else if (a[left] > a[right])
return left;
else
return right;
}
else
{
if (a[mid] > a[right])
return mid;
else if (a[left] < a[right])
return left;
else
return right;
}
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
if (right - left <= 8)
{
InsertSort(a + left, right - left + 1);
}
else
{
int keyi = PartSort2(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
}
int PartSort2(int* a, int left, int right)
{
int mid = getmidindex(a, left, right);
Swap(&a[left], &a[mid]);
int key = a[left];
int hole = left;
while (left < right)
{
while (left < right && a[right] >= key)
--right;
a[hole] = a[right];
hole = right;
while (left < right && a[left] <= key)
++left;
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
排序性能对比
void testOp()
{
srand((unsigned int)time(0));
const int N = 10000;
int* a1 = (int*)malloc(sizeof(int) * N);
assert(a1);
int* a2 = (int*)malloc(sizeof(int) * N);
assert(a2);
int* a3 = (int*)malloc(sizeof(int) * N);
assert(a3);
int* a4 = (int*)malloc(sizeof(int) * N);
assert(a4);
int* a5 = (int*)malloc(sizeof(int) * N);
assert(a5);
int* a6 = (int*)malloc(sizeof(int) * N);
assert(a6);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
}
int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
int begin3 = clock();
SelectSort(a3, N);
int end3 = clock();
int begin4 = clock();
HeapSort(a4, N);
int end4 = clock();
int begin5 = clock();
BubbleSort(a5, N);
int end5 = clock();
int begin6 = clock();
QuickSort(a6, 0, N - 1);
int end6 = clock();
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
printf("BubbleSort:%d\n", end5 - begin5);
printf("QuickSort:%d\n", end6 - begin6);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
a1 = NULL;
a2 = NULL;
a3 = NULL;
a4 = NULL;
a5 = NULL;
a6 = NULL;
}
int main()
{
testOp();
return 0;
}
当N为100000时 当N为1000000时
1.4 快速排序非递归
快速排序非递归实现,需要借助栈,栈中存放的是需要排序的左右区间。 而且非递归可以彻底解决栈溢出的问题。 其实他的思想与递归是类似的:
1.4.1 具体思路
将数组左右下标入栈, 若栈不为空,两次取出栈顶元素,分别为闭区间的左右界限 将区间中的元素按照前后指针法排序(其余两种也可)得到基准值的位置 再以基准值为界限,若基准值左右区间中有元素,则将区间入栈 重复上述步骤直到栈为空。
代码
void QuickSortNonR(int* a, int begin, int end)
{
ST s;
StackInit(&s);
StackPush(&s, begin);
StackPush(&s, end);
while (!StackEmpty(&s))
{
int right = StackTop(&s);
StackPop(&s);
int left = StackTop(&s);
StackPop(&s);
int keyi = PartSort1(a, left, right);
if (keyi + 1 < right)
{
StackPush(&s, keyi + 1);
StackPush(&s, right);
}
if (left < right - 1)
{
StackPush(&s, left);
StackPush(&s, keyi - 1);
}
}
StackDestory(&s);
}
1.5 快速排序的特性总结
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
2.写在最后
那么八大排序(二)——快速排序&&冒泡排序就到这里啦。
|