目录
Ⅰ.冒泡排序
1.基本思想
反复交换相邻两个元素的位置,从而把待排序的元素序列逐个比较并按照大小重新排列。
即
2.代码实现
我们分析发现
在每一趟中都要保证本趟中最大的值处于本次的最后,保证排出升序
// 冒泡排序
void BubbleSort(int* a, int n)
{
int j = 0;
for(j = 1; j < n; j++)//进行n-1趟排序
{
int i = 0;
for (i = 0; i < n - j; i++)//得到从开始到其后的n-j个数据的下标
{
//不断地将本趟中最大的值向后移动
//保证每趟的最后一个数据一定是本趟中最大的数值
if (a[i] > a[i + 1])
{
Swap(&a[i], &a[i + 1]);
}
}
}
ArrPrint(a, n);
}
我们用以下代码来验证
void test1()
{
int a[] = { 8,6,4,9,7,1,5,3,2,0 };
int sz = sizeof(a) / sizeof(a[0]);
BubbleSort(a, sz);
}
int main()
{
test1();
return 0;
}
有
3.总结
1. 冒泡排序是一种非常容易理解的排序2. 时间复杂度: O(N^2)3. 空间复杂度: O(1)4. 稳定性:稳定
Ⅱ.快速排序
1.基本思想
任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止 。
图示如下
因此我们可以构建一个基础的通用代码模型
// 假设按照升序对a数组中[left, right]区间中的元素进行排序
void QuickSort(int* a, int left, int right)
{
//如果最左的下标与最右重叠(当前仅有一个值)或大于最右(不存在)时就结束
if (left >= right)
return;
// 按照基准值对a数组的 [left, right]区间中的元素进行划分
int key = PartSortn(a, left, right);
// 划分成功后以key为边界形成了三部分 [left, key-1]、key和 [key+1, right]
// 递归排[left, key-1]
QuickSort(a, left, key-1);
// 递归排[key+1, right]
QuickSort(a, key + 1, right);
}
2.hoare法
基本思想:
首先从数列中选择一个基准值。
将数列中小于等于基准值的元素放在基准值的左侧,将大于等于基准值的元素放在基准值的右侧。
对左右两个子序列分别重复以上步骤,直到每个子序列只剩下一个元素为止。
即
代码实现:
//取三数中的中间值
int GetMidNumi(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])//left<mid
{
if (a[mid] < a[right])//left<mid<right
{
return mid;
}
else if (a[left] > a[right])//right<left<mid
{
return left;
}
else//left<right<mid
{
return right;
}
}
else//left>mid
{
if (a[mid] > a[right])//left>mid>right
{
return mid;
}
else if (a[left] < a[right])//right>left>mid
{
return left;
}
else//left>right>mid
{
return right;
}
}
}
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
//取左右中三个数中的最中间值
int midi = GetMidNumi(a, left, right);
if (midi != left)
{
Swap(&a[midi], &a[left]);
}
int keyi = left;
while (left<right)//left与right不相遇就继续循环
{
//右边找到小(或等)或者左右相遇停止,
//没找到就继续
while (left < right && a[right] >= a[keyi])
right--;
//左边找到大(或等)或者左右相遇停止,
//没找到就继续
while (left < right && a[left] <= a[keyi])
left++;
//交换找到的值,将小值放在左边,大值放在右边
Swap(&a[left], &a[right]);
}
//交换key与左值
Swap(&a[keyi], &a[left]);
//左值与key交换之后,处于left位置的数值已经处于正确的位置
return left;
}
我们用如下代码测试
void test2()
{
int a[] = { 8,6,4,9,7,1,5,3,2,0 };
int sz = sizeof(a) / sizeof(a[0]);
QuickSort(a, 0, sz - 1);
ArrPrint(a, sz);
}
int main()
{
test2();
return 0;
}
有
3.挖坑法
基本思想:
选取数组的一个元素作为基准值 key。
从数组的最右端开始向左遍历,寻找第一个小于 key 的元素,将其挖出,并将该位置留空作为坑。
从数组的最左端开始向右遍历,寻找第一个大于 key 的元素,将其挖出,并填入刚刚留下的坑中。
重复执行步骤 2 和步骤 3,直到左右两个遍历指针相遇。
将基准值 key 填入最后一个坑中,此时 key 左侧所有元素都小于 key,右侧所有元素都大于等于 key。
对左右两个子序列分别重复以上步骤,直到每个子序列只剩下一个元素为止。
图示如下
代码实现:
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
//取左右中三个数中的最中间值
int midi = GetMidNumi(a, left, right);
if (midi != left)
{
Swap(&a[midi], &a[left]);
}
//将基准值保存,并设置坑位
int key = a[left];
int hole = left;
while (left < right)//左右未相遇就继续寻找
{
//右边找小
while (left < right && key < a[right])
--right;
//找到小或者相遇时停止,将找到的数放入坑中并将坑置于right处
a[hole] = a[right];
hole = right;
//左边找大
while (left<right && key>a[left])
++left;
//找到大或者相遇时停止,将找到的数放入坑中并将坑置于left处
a[hole] = a[left];
hole = left;
}
//将key值填入hole中
a[hole] = key;
//此时hole处所在位置的值已经处于正确位置
return hole;
}
还是采用hoare法的检测代码,有
4.前后指针法
基本思想:
选取数组的一个元素作为基准值 pivot。
设置两个指针,一个指向数组的第一个元素(即前指针),一个指向 pivot(即后指针)。
从前指针开始向后遍历,寻找第一个大于等于 pivot 的元素。
将该元素与后指针指向的元素交换位置,后指针向后移动一个位置。
重复执行步骤 3 和步骤 4,直到前指针遍历到数组的最后一个元素。
将 pivot 与后指针指向的元素交换位置,此时 pivot 左侧所有元素都小于 pivot,右侧所有元素都大于等于 pivot。
对左右两个子序列分别重复以上步骤,直到每个子序列只剩下一个元素为止。
图示如下
代码实现:
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
//取左右中三个数中的最中间值
int midi = GetMidNumi(a, left, right);
if (midi != left)
{
Swap(&a[midi], &a[left]);
}
int keyi = left;
int cur = left + 1;//第二个数作cur
int prev = left;//第一个数作prev
while (cur <= right)//cur不越界就继续
{
//找小,如果当前值小于key值且prev增加后不与cur相同
//(如果相同说明prev与cur紧邻,此时只需对cur++即可)
//交换key与cur的值
if (a[keyi] > a[cur] && prev++ != cur)
Swap(&a[cur], &a[prev]);
cur++;
}
//cur出界后,交换keyi与prev所在位置的值
Swap(&a[keyi], &a[prev]);
//此时,prev所处位置的值已处于正确位置
return prev;
}
还是采用上述的代码检验,有
5.非递归法
基本思想:
将待排序序列的起始位置和结束位置压入栈中,作为当前子序列的起始状态。
循环执行以下操作,直到栈为空:
- 弹出栈顶元素,得到当前子序列的起始位置 left 和结束位置 right。
- 对子序列进行一次快速排序,得到基准值的位置 keyi。
- 如果基准值左侧子序列元素个数大于 1,将左侧子序列的起始位置 left 和结束位置 keyi-1 压入栈中,等待下一轮排序。
- 如果基准值右侧子序列元素个数大于 1,将右侧子序列的起始位置 keyi+1 和结束位置 right 压入栈中,等待下一轮排序。
排序完成后,原序列就变成了有序的序列。
图示如下
在对0(begin)到9(end)进行一趟快排后,得到一个基准值key,如果左右两侧元素均不只有一个元素,将key左侧起始位置(begin)到key-1(end)位置压栈,再将key右侧keyi+1(begin)位置到右侧结束位置(end)压栈
代码实现:
// 快速排序 非递归实现(借助栈实现)
void QuickSortNonR(int* a, int left, int right)
{
//创建并初始化栈后,分别将left与right分别压栈
ST st;
STInit(&st);
STPush(&st, left);
STPush(&st, right);
while (!STEmpty(&st))//当栈不为空时,重复进行快排
{
//分别取得本区域内的起始位置与结束位置
int end = STTop(&st);
STPop(&st);
int begin = STTop(&st);
STPop(&st);
//对本区域的数据进行一次快排
int keyi = PartSort3(a, begin, end);
//得到[begin,keyi-1],keyi,[keyi+1,end]三个区域
//将keyi左边的起始位置begin与结束位置keyi-1分别压栈
if (begin < keyi - 1)
{
STPush(&st, begin);
STPush(&st, keyi - 1);
}
//将keyi右边的起始位置keyi+1与结束位置end分别压栈
if (keyi + 1 < end)
{
STPush(&st, keyi + 1);
STPush(&st, end);
}
}
}
我们使用如下的检测代码
void test3()
{
int a[] = { 8,6,4,9,7,1,5,3,2,0 };
int sz = sizeof(a) / sizeof(a[0]);
QuickSortNonR(a, 0, sz - 1);
ArrPrint(a, sz);
}
int main()
{
test3();
return 0;
}
有
6.快速排序的优化与改进
其实对于快速排序,我们可以发现它的整体是类似于完全二叉树的形式,而在数据量较少时,我们可以采取效率更高的插入排序,即
// 假设按照升序对a数组中[left, right]区间中的元素进行排序
void QuickSort(int* a, int left, int right)
{
//如果最左的下标与最右重叠(当前仅有一个值)或大于最右(不存在)时就结束
if (left >= right)
return;
//当小区间数据量小于一定量时,使用插入排序
if (right - left < 15)
{
InsertSort(a+left,right-left);
}
// 按照基准值对a数组的 [left, right]区间中的元素进行划分
int key = PartSort3(a, left, right);
// 划分成功后以key为边界形成了三部分 [left, key-1]、key和 [key+1, right]
// 递归排[left, key-1]
QuickSort(a, left, key-1);
// 递归排[key+1, right]
QuickSort(a, key + 1, right);
}
在使用了插入排序后,我们可以发现对于一整个快速排序而言,最后一排的数据量近乎占据了整个数据量的一半,因此对栈帧的开辟作出了一定的优化。
7.总结
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫 快速 排序2. 时间复杂度: O(N*logN)3. 空间复杂度: O(logN)4. 稳定性:不稳定