基本思想:
通过不断地比较相邻的元素,将较大的元素往后移动,从而实现排序的目的。
具体的步骤如下:
冒泡排序的特点是每次只比较相邻的两个元素,每一轮排序都将最大的元素移动到最后,因此称为冒泡。整个排序过程类似于水泡从底部冒到顶部的过程,因此得以闻名.
//冒泡排序
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n; i++)
{
int exchange = 1;
for (int j = 1; j < n - i; j++)
{
if (a[j - 1] > a[j])
{
Swap(&a[j - 1], &a[j]);
exchange = 0;
}
}
if (exchange)
return;
}
}
这里面有一个小小的优化,exchange如果在这一趟排序中没有被修改过,代表着这组数据是有序的,因此可以直接return返回.
代码解析:
冒泡排序的特性总结:
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止 .
void QuickSort(int* a, int left, int right)
{
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
if (right <= left)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
//int keyi = PartSort1(a, left, right);
//int keyi = PartSort2(a, left, right);
int keyi = PartSort3(a, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, keyi-1) 和 [keyi+1, right)
// 递归排[left, keyi-1)
QuickSort(a, left, keyi - 1);
// 递归排[keyi+1, right)
QuickSort(a, keyi + 1, right);
}
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,在写递归框架时想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。
int PartSort1(int* a, int left, int right)
{
//三数取中(优化)
//int keyi = NumBers(a, left, right);
//Swap(&a[keyi], &a[left]);
int key = left;
while (left < right)
{
while (left < right && a[left] <= a[right])
{
right--;
}
while (left < right && a[left] <= a[right])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[key]);
return left;
}
代码解析:
实现了一次快速排序的分割操作,将数组分成两部分,左边的元素都小于等于基准值,右边的元素都大于基准值。然后再通过递归调用这个函数,这就是hoare版的快排.
int PartSort2(int* a, int left, int right)
{
//三数取中优化
//int keyi = NumBers(a, left, right);
//Swap(&a[keyi], &a[left]);
int key = a[left];
int hole = left;//为第一个坑
while (left < right)
{
while (left < right && key <= a[right])
{
--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;
}
代码解析:
同样实现了将数组分成两部分,左边的元素都小于等于基准值,右边的元素都大于基准值。
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
//三数取中优化
//int midi = NumBers(a, left, right);
//Swap(&a[left], &a[midi]);
int prev = left;
int cur = prev + 1;
int keyi = left;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
代码解析:
同样实现了将数组分成两部分,左边的元素都小于等于基准值,右边的元素都大于基准值。
以上三种方法均可实现快速排序,没有谁优谁劣,挑取自己便于理解的就行
三数取中是为了选择一个更好的基准值,以提高快速排序的效率。在快速排序中,选择一个合适的基准值是非常重要的,它决定了每次分割的平衡性。
快速排序的基本思想是通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的小,然后再对这两部分分别进行快速排序,递归地进行下去,直到整个序列有序。
如果每次选择的基准值都是最左边或最右边的元素,那么在某些情况下,快速排序的效率可能会降低。例如,当待排序序列已经有序时,如果每次选择的基准值都是最左边或最右边的元素,那么每次分割得到的两个子序列的长度差可能会非常大,导致递归深度增加,快速排序的效率降低。
而通过三数取中的优化,可以选择一个更好的基准值,使得每次分割得到的两个子序列的长度差更小,从而提高快速排序的效率。
具体来说,三数取中的优化是选择待排序序列的左端、右端和中间位置的三个元素,然后取它们的中值作为基准值。这样选择的基准值相对于最左边或最右边的元素,更接近整个序列的中间位置,可以更好地平衡分割后的两个子序列的长度,从而提高快速排序的效率。
通过三数取中的优化,可以减少递归深度,提高分割的平衡性,使得快速排序的效率更稳定,适用于各种不同的输入情况。
//三数取中
int NumBers(int* a, int left, int right)
{
int mid = (left + right) / 2;
// left mid right
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right]) // mid是最大值
{
return left;
}
else
{
return right;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right]) // mid是最小
{
return left;
}
else
{
return right;
}
}
}
小区间优化是指在快速排序中,当待排序的子序列的长度小于一定阈值时,不再继续使用快速排序,而是转而使用插入排序。
void QuickSort(int* a, int left, int right)
{
if (right <= left)
return;
if(right - left + 1 > 10)
{
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
else
{
InsertSort(a + left,right - left + 1);
}
}
小区间优化的好处:
小区间优化可以在一定程度上提高快速排序的性能。它通过减少递归深度、提高局部性和减少分割次数来优化算法的效率,特别适用于处理较小的子序列。
这里需要借助栈的来实现非递归.关于栈详情见:数据结构剖析–栈
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
Stack st;
StackInit(&st);
StackPush(&st, right);
StackPush(&st, left);
while (!StackEmpty(&st))
{
int begin = StackTop(&st);
StackPop(&st);
int end = StackTop(&st);
StackPop(&st);
int keyi = PartSort3(a, begin, end);
if (keyi + 1 < end)
{
StackPush(&st, end);
StackPush(&st, keyi + 1);
}
if (begin < keyi - 1)
{
StackPush(&st, keyi - 1);
StackPush(&st, begin);
}
}
StackDestroy(&st);
}
代码解析:
通过使用栈来模拟递归的过程,非递归实现避免了递归调用的开销,提高了快速排序的效率。
快速排序的特性总结: