基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。
交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
冒泡排序(Bubble Sort)是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢 “浮” 到数列的顶端。
冒泡排序有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。
什么时候最快?
当输入的数据已经是正序时(都已经是正序了,我还要你冒泡排序有何用啊)。
什么时候最慢?
当输入的数据是反序时(写一个 for 循环反序输出数据不就行了,干嘛要用你冒泡排序呢,我是闲的吗)。
void Swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
// 冒泡排序
// 时间复杂度:O(N^2)
// 最好情况是多少:O(N)
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
bool exchange = false;
for (int j = 1; j < n - i; j++)
{
if (a[j - 1] > a[j])
{
Swap(&a[j - 1], &a[j]);
exchange = true;
}
}
if (exchange == false)
break;
}
}
O(N^2)
O(1)
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
建议:此处优化内容可以在学完一两个快排思路后再进行观看。
快速排序优化:
O(n^2)
。通过三数取中,可以减少选择到极端值作为基准值的可能性,从而提高算法的平均性能。总的来说,三数取中是快速排序中的一种优化策略,旨在通过选择一个更好的基准值来提高排序效率,减少最坏情况的发生概率,并使算法更加稳定和鲁棒。
【三数取中代码实现】:
算法步骤:
midi
。a[begin] <= a[midi]
),则:
// 三数取中
int GetMidi(int* a, int begin, int end)
{
int midi = (begin + end) / 2;
// begin midi end 三个数选中位数
if (a[begin] > a[midi])
{
if (a[midi] > a[end])
return midi;
else if (a[begin] > a[end])
return end;
else
return begin;
}
else // a[begin] <= a[midi]
{
if (a[midi] < a[end])
return midi;
else if (a[begin] < a[end])
return end;
else // a[end] <= a[begin] <= a[midi]
return begin;
}
}
综上所述,小区间优化是快速排序中一个重要的性能提升措施,它有助于降低算法在实际应用中的最坏情况时间复杂度,提高整体的排序效率。
【小区间优化快排代码】:
// 快排(霍尔法 + 小区间优化)
void QuickSort1(int* a, int begin, int end)
{
if (begin >= end)
return;
// 小区间优化
if (end - begin + 1 <= 10) // 数组元素个数为小于10时调用直接插入排序
{
InsertSort(a + begin, end - begin + 1);
}
else
{
// 三数取中,优化快排
// begin midi end 三个数选中位数
int midi = GetMidi(a, begin, end);
Swap(&a[begin], &a[midi]);
int left = begin, right = end;
int keyi = begin;
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;
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
}
算法步骤:
- 选择基准值:从待排序数列中选取第一个数作为基准值。
- 分区:重新排列数列,所有比基准值小的元素移到基准值的左边,所有比基准值大的元素移到基准值的右边。这个过程称为一趟快速排序。
- 递归排序:对基准值左、右两边的子序列重复上述步骤,直到所有子序列长度不超过
1
,此时待排序数列就变成了有序数列。
void Swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
// 快速排序递归实现
// hoare版快排的单趟排序
int PartSort1(int* a, int begin, int end)
{
// 三数取中,优化快排
// begin midi end 三个数中选中位数
int midi = GetMidi(a, begin, end);
Swap(&a[begin], &a[midi]);
int left = begin, right = end;
int keyi = begin;
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]);
return left;
}
// 快排
void QuickSort(int* a, int begin, int end)
{
// 递归出口
if (begin >= end)
return;
int keyi = PartSort1(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
算法步骤:
- 选择基准值:从待排序数列中选取第一个数作为基准值。
- 挖坑:将基准值放在待排序数列中的一个“坑”的位置上,这个“坑”的位置相当于基准值的索引。
- 分区:将比基准值小的元素移到基准值的左边,将比基准值大的元素移到基准值的右边。这一步实际上是在填平“坑”的过程,同时也使得基准值左边的元素都比它小,右边的元素都比它大。
- 递归排序:对基准值左、右两边的子序列重复上述步骤,直到所有子序列长度不超过
1
,此时待排序数列就变成了有序数列。
void Swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
// 挖坑法
int PartSort2(int* a, int begin, int end)
{
// 三数取中,优化快排
// begin midi end 三个数选中位数
int midi = GetMidi(a, begin, end);
Swap(&a[begin], &a[midi]);
int key = a[begin]; // 保存基准值
int hole = begin; // 初始化坑位为基准值的下标
while (begin < end)
{
// 右遍找小,填到左边的坑
while (begin < end && a[end] >= key)
{
--end;
}
a[hole] = a[end]; // 填坑
hole = end; // 更新坑位
// 左边找大,填到右边的坑
while (begin < end && a[begin] <= key)
{
++begin;
}
a[hole] = a[begin]; // 填坑
hole = begin; // 更新坑位
}
a[hole] = key; // 将基准值归位
return hole; // 返回基准值的正确下标
}
// 快排
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int keyi = PartSort2(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
算法步骤:
- 选择基准值:从待排序数列中选取第一个数作为基准值
(key)
。- 创建前后指针:创建两个指针,
prev
和cur
,prev
指向数组序列首元素位置,cur
指向prev
的下一个位置。- 遍历:通过遍历,如果
cur
指针找到比key
小的数,然后判断prev
的下一个位置是否为cur
所处的位置,如果不是,则将两个值进行交换,如果是,则继续往后遍历,直到cur
遍历结束。- 归位:最后要将
key
基准值与prev
指向的值进行互换,最终确认基准值处于数组序列的中间位置。- 递归排序:递归地对基准值左边和右边的子数组进行排序。
void Swap(int* p1, int* p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
// 前后指针法
int PartSort3(int* a, int begin, int end)
{
// 三数取中,优化快排
// begin midi end 三个数选中位数
int midi = GetMidi(a, begin, end);
Swap(&a[begin], &a[midi]);
int keyi = begin;
int prev = begin;
int cur = prev + 1;
while (cur <= end)
{
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
// 快排
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int keyi = PartSort3(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
非递归
快速排序的非递归实现可以使用栈来模拟递归过程。以下是算法步骤和思想:
算法步骤:
存储待处理的子数组的起始和结束位置
。栈不为空时
,执行以下操作:
弹出栈顶元素,即当前待处理的子数组的起始和结束位置
。左右两个子数组起始位置小于结束位置则将左右两个子数组的起始和结束位置入栈
。将左子数组的起始和结束位置以及右子数组的起始和结束位置入栈
。直到栈为空,此时整个数组已经有序
。算法思想:
总的来说,快速排序的非递归实现利用了栈来模拟递归过程,通过迭代的方式对整个数组进行遍历和排序。这种方法可以避免函数调用的开销,并且可以更直观地理解快速排序的工作原理。
辅助栈代码:
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top; // 标识栈顶位置的
int capacity;
}ST;
// 初始化栈
void STInit(ST* pst)
{
assert(pst);
pst->a = NULL;
pst->capacity = 0;
// 表示top指向栈顶元素的下一个位置
pst->top = 0;
// 表示top指向栈顶元素
//pst->top = -1;
}
// 栈的销毁
void STDestroy(ST* pst)
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->top = pst->capacity = 0;
}
// 栈顶插入删除
void STPush(ST* pst, STDataType x)
{
assert(pst);
if (pst->top == pst->capacity)
{
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, sizeof(STDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
pst->a = tmp;
pst->capacity = newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
// 栈顶元素出栈
void STPop(ST* pst)
{
assert(pst);
// 不为空
assert(pst->top > 0);
pst->top--;
}
// 取栈顶元素
STDataType STTop(ST* pst)
{
assert(pst);
// 不为空
assert(pst->top > 0);
return pst->a[pst->top - 1];
}
// 判断栈空
bool STEmpty(ST* pst)
{
assert(pst);
/*if (pst->top == 0)
{
return true;
}
else
{
return false;
}*/
return pst->top == 0;
}
// 获取栈的大小
int STSize(ST* pst)
{
assert(pst);
return pst->top;
}
快排的非递归代码:
// 快排(非递归)
void QuickSortNonR(int* a, int begin, int end)
{
ST s;
STInit(&s);
// 把区间 [begin, end]压栈
STPush(&s, end);
STPush(&s, begin);
while (!STEmpty(&s))
{
int left = STTop(&s);
STPop(&s);
int right = STTop(&s);
STPop(&s);
int keyi = PartSort3(a, left, right); // 三种方法都可
// [left, keyi-1] keyi [keyi+1, right]
// 判断当前基准值左右区间是否满足条件(即区间内的元素个数是否大于1),满足就将区间位置压入栈中
if (keyi + 1 < right)
{
STPush(&s, right);
STPush(&s, keyi + 1);
}
if (left < keyi - 1)
{
STPush(&s, keyi - 1);
STPush(&s, left);
}
}
STDestroy(&s);
}