基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
2.1 冒泡排序
2.1.1 冒泡排序思想
冒泡排序(Bubble Sort)是一种简单直观的排序算法,其基本思想如下:
- 比较相邻元素:从序列的第一个元素开始,依次比较相邻的两个元素。如果前一个元素比后一个元素大,就交换它们的位置。
- 重复步骤:对每一对相邻元素进行同样的操作,从序列的开始到结尾。这样,最大的元素会逐渐“冒泡”到序列的末尾。
- 多次遍历:重复上述过程,忽略已经排好序的最后一个元素,直到整个序列有序
通过这种方法,较大的元素会逐渐从前向后移动,就像水中的气泡一样逐渐上升,因此称为“冒泡排序”
2.1.2 冒泡排序步骤
具体步骤如下:
-
第一轮排序:
- 比较第1和第2个元素,如果第1个元素大于第2个元素,则交换它们的位置。
- 比较第2和第3个元素,如果第2个元素大于第3个元素,则交换它们的位置。
- 依次类推,直到比较最后两个元素。
-
第二轮排序:
- 忽略已经排好序的最后一个元素,重复第一轮的比较和交换过程。
-
后续轮次:
- 每一轮排序后,忽略已经排好序的最后一个元素,继续进行比较和交换,直到没有元素需要交换为止。
2.1.3 冒泡排序代码
void BubbleSort(int* a, int n)
{
// 外层循环:控制排序的轮数,从数组末尾向前遍历
for (int i = n; i > 0; i--)
{
// 内层循环:比较相邻的元素并交换它们的位置
for (int j = 0; j < i - 1; j++)
{
// 如果前一个元素大于后一个元素,则交换它们的位置
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]); // 调用Swap函数交换元素
}
}
}
}
2.2快速排序
2.2.1 快速排序思想
快速排序(Quick Sort)的基本思想是“分治法”,即通过递归地将一个大问题分解为多个小问题来解决。具体来说:
- 选择基准元素:从待排序的数组中选择一个元素作为基准(pivot)。
- 分区:将数组中小于基准元素的元素移到基准元素的左边,将大于基准元素的元素移到基准元素的右边。这样,基准元素就处于其最终位置。
- 递归排序:对基准元素左边和右边的子数组分别进行同样的操作,直到每个子数组只有一个元素或为空。
通过这种方法,快速排序能够高效地将一个无序数组排序为有序数组。它的平均时间复杂度为 (O(n \log n)),在大多数情况下表现非常优异。
整体代码框架
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
if(right - left <= 1)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int div = partion(array, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
// 递归排[left, div)
QuickSort(array, left, div);
// 递归排[div+1, right)
QuickSort(array, div+1, right);
}
2.2.2 快速排序步骤
2.2.2.1 hoare版本
1 具体步骤如下:
-
选择基准元素:
- 选择数组中的一个元素作为基准值(pivot)。
-
分区过程:
- 设置两个指针,分别指向数组的两端。
- 从右向左移动右指针,找到第一个小于基准值的元素。
- 从左向右移动左指针,找到第一个大于基准值的元素。
- 交换这两个元素的位置。
- 重复上述过程,直到左右指针相遇。
-
递归排序:
- 对基准值左边的子数组和右边的子数组分别进行递归排序。
-
合并结果:
- 当所有子数组都有序时,整个数组就自然有序了。
2 代码
int PartSort1(int* a, int left, int right)
{
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]);
return left;
}
2.2.2.2 挖坑法
1 具体步骤如下:
挖坑法是一种快速排序的实现方法,其基本思想是通过“挖坑填坑”的方式来实现分区。具体步骤如下:
- 选择基准值:选择子数组的第一个元素作为基准值(pivot),并将其暂存到一个变量中,这个位置就成为第一个“坑”。
- 分区操作:
- 从右向左扫描,找到第一个小于基准值的元素,将其填入当前的“坑”中,并将该位置作为新的“坑”。
- 从左向右扫描,找到第一个大于基准值的元素,将其填入当前的“坑”中,并将该位置作为新的“坑”。
- 重复上述过程,直到左右指针相遇。
- 填入基准值:将基准值填入最后的“坑”中,此时基准值左边的元素都小于它,右边的元素都大于它。
- 递归排序:对基准值左边和右边的子数组分别进行递归排序。
2 代码
// 挖坑法
int PartSort2(int* a, int begin, int end)
{
int midi = GetMidi(a, begin, end); // 获取中间值的索引
Swap(&a[midi], &a[begin]); // 将中间值与第一个元素交换
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; // 返回基准值的位置
}
2.2.2.3 前后指针版本

1 具体步骤如下:
- 选择基准值:选择子数组的第一个元素作为基准值(pivot)。
- 分区操作:
- 使用两个指针,
prev
和cur
。prev
指向基准值的位置,cur
从prev
的下一个位置开始。 cur
向前移动,如果cur
指向的元素小于基准值,则将prev
向前移动一位,并交换prev
和cur
指向的元素。- 如果
cur
指向的元素大于基准值,则cur
继续向前移动,prev
不动。 - 重复上述过程,直到
cur
越界。
- 使用两个指针,
- 交换基准值:最后将基准值与
prev
指向的元素交换位置。 - 递归排序:对基准值左边和右边的子数组分别进行递归排序。
2 代码
int Partition(int* a, int left, int right)
{
int pivot = a[left]; // 选择第一个元素作为基准值
int prev = left; // 初始化prev指针为基准值位置
int cur = left + 1; // 初始化cur指针为基准值的下一个位置
while (cur <= right)
{
if (a[cur] < pivot)
{
prev++;
Swap(&a[prev], &a[cur]); // 交换prev和cur指向的元素
}
cur++;
}
Swap(&a[left], &a[prev]); // 将基准值与prev指向的元素交换
return prev; // 返回基准值的位置
}
2.2.2.4 快速排序非递归
1 具体步骤如下:
- 初始化栈:创建一个栈,用于存储需要排序的子数组的左右边界。
- 入栈:将整个数组的左右边界(即初始的
left
和right
)入栈。 - 循环排序:
- 从栈中弹出左右边界,进行分区操作。
- 将分区后的左右子数组的边界分别入栈。
- 重复上述过程,直到栈为空。
2 代码
void QuickSortNonR(int* a, int begin, int end)
{
Stack s; // 定义一个栈用于存储子数组的左右边界
StackInit(&s); // 初始化栈
StackPush(&s, end); // 将初始右边界入栈
StackPush(&s, begin); // 将初始左边界入栈
while (!StackEmpty(&s)) // 当栈不为空时,继续排序
{
int left = StackTop(&s); // 获取栈顶元素作为左边界
StackPop(&s); // 弹出栈顶元素
int right = StackTop(&s); // 获取新的栈顶元素作为右边界
StackPop(&s); // 弹出栈顶元素
int keyi = PartSort3(a, left, right); // 对子数组进行分区操作,返回基准值的位置
// [left, keyi-1] keyi [keyi+1, right]
if (left < keyi - 1) // 如果左子数组存在元素
{
StackPush(&s, keyi - 1); // 将左子数组的右边界入栈
StackPush(&s, left); // 将左子数组的左边界入栈
}
if (keyi + 1 < right) // 如果右子数组存在元素
{
StackPush(&s, right); // 将右子数组的右边界入栈
StackPush(&s, keyi + 1); // 将右子数组的左边界入栈
}
}
StackDestroy(&s); // 销毁栈,释放资源
}
2.2.3 快速排序的特性总结:
快速排序(Quicksort)是一种高效的排序算法,以下是其主要特性总结:
-
基本思想:
- 快速排序采用分治法,将一个大问题分解为多个小问题来解决。
- 通过选择一个基准值(pivot),可以将数组分为两部分:一部分包含小于基准值的元素,另一部分包含大于基准值的元素,然后对这两部分分别递归进行排序。
-
时间复杂度:
- 平均时间复杂度:
O(nlogn),其中n是数组的长度。
- 最坏时间复杂度:
O(
),当每次选择的基准值都是数组的最大或最小值时会出现这种情况
- 最好时间复杂度:
O(nlogn),当每次选择的基准值都能将数组均匀分割时
- 平均时间复杂度:
-
空间复杂度:
- 快速排序的空间复杂度为
O(logn),主要用于递归调用栈。
- 快速排序的空间复杂度为
-
稳定性:
快速排序是一种不稳定的排序算法,因为相同元素的相对位置可能会改变 -
适用场景:
快速排序适用于大多数情况下的排序,尤其是当数据量较大且对稳定性要求不高时