目录
一、交换排序的总体概念
交换排序是一类排序算法,它的核心思想是通过交换元素的位置来达到排序的目的。在排序过程中,比较数组中的元素对,如果它们的顺序不符合排序要求,就交换它们的位置。在这里主要讲冒泡排序和快速排序。
二、冒泡排序
-
基本概念:
- 冒泡排序是一种简单的交换排序算法。它的基本思想是通过反复比较相邻的元素,根据排序要求(升序或降序)交换它们的位置,使得每一轮比较后,最大(或最小)的元素像气泡一样 “浮” 到数组的一端。
-
工作原理:
- 对于一个包含 n 个元素的数组,进行 n - 1 轮比较。在每一轮比较中,从数组的第一个元素开始,依次比较相邻的两个元素。如果它们的顺序不符合排序要求(例如在升序排序中,前面的元素大于后面的元素),则交换这两个元素的位置。这样,在每一轮比较结束后,未排序部分的最大元素就会被交换到该轮比较范围的末尾。
- 随着比较轮数的增加,每一轮需要比较的元素数量逐渐减少。例如,第一轮需要比较 n - 1 对相邻元素,第二轮需要比较 n - 2 对相邻元素,以此类推,最后一轮只需要比较 1 对相邻元素。
-
特点:
- 时间复杂度:
- 最坏情况和平均情况的时间复杂度都是O(n*n)。当数组是逆序(对于升序排序要求)时,每一轮比较都需要进行最多的交换操作,总共需要进行 n - 1 轮比较,所以时间复杂度为O(n*n)。
- 最好情况时间复杂度为O(n),当数组已经有序时,只需要进行一轮比较,且不进行任何交换操作,就可以确定数组已经有序。
- 空间复杂度:空间复杂度为O(1),因为它只需要一个临时变量来辅助交换相邻元素,属于原地排序算法。
- 稳定性:冒泡排序是一种稳定的排序算法。在比较相邻元素时,如果两个元素相等,不进行交换,所以相同元素的相对顺序不会改变。
- 时间复杂度:
-
主要步骤
- 外层循环控制遍历趟数:
for (int j = 0; j < n; ++j)
外层循环控制整个排序过程的趟数。每一趟都会将当前未确定位置的最大元素移动到正确的位置上。随着趟数的增加,已确定位置的元素越来越多,未确定位置的元素越来越少。
- 内层循环进行相邻元素比较和交换:
int exchange = 0;
在内层循环开始前,初始化一个标志变量exchange
为 0,表示在当前这一趟中还没有进行过交换。for (int i = 1; i < n - j; ++i)
内层循环从数组的第二个元素开始,依次比较相邻的两个元素。如果前一个元素大于后一个元素,就进行交换。if (a[i - 1] > a[i])
如果a[i - 1]
大于a[i]
,则执行交换操作Swap(&a[i - 1], &a[i])
,并将exchange
设置为 1,表示在这一趟中有过交换。
- 提前结束排序的条件:
if (exchange == 0)
在每一趟遍历结束后,检查exchange
的值。如果exchange
为 0,说明在这一趟中没有进行过交换,即数组已经有序,此时可以提前结束排序。
-
//冒泡排序 void BubbleSort(int* a, int n) { //控制躺数 for (int j = 0; j < n; ++j) { int exchange = 0; //控制交换次数 for (int i = 1; i < n - j; ++i) { if (a[i - 1] > a[i]) { Swap(&a[i - 1], &a[i]); exchange = 1; } } if (exchange == 0) { break; } } }
三、快速排序
-
基本概念:
- 快速排序是一种高效的交换排序算法,它基于分治策略。其基本思想是选择一个基准元素(pivot),将数组分为两部分,使得左边部分的元素都小于等于基准元素,右边部分的元素都大于等于基准元素,然后对这两部分分别进行快速排序,直到整个数组有序。
-
工作原理:
- 首先选择一个基准元素,通常可以选择数组的第一个元素、最后一个元素或中间元素等。以选择第一个元素为基准为例。
- 设置两个指针,一个从数组的第二个元素开始(左指针),一个从数组的最后一个元素开始(右指针)。左指针向右移动,寻找大于基准元素的元素;右指针向左移动,寻找小于基准元素的元素。当左指针找到大于基准元素的元素,右指针找到小于基准元素的元素时,交换这两个元素的位置。
- 不断重复上述步骤,直到左指针和右指针相遇。此时,将基准元素与相遇位置的元素交换,这样就将数组分为了两部分,左边部分的元素都小于等于基准元素,右边部分的元素都大于等于基准元素。
- 然后对这两部分子数组分别递归地应用快速排序算法,直到子数组的长度为 1 或 0,此时数组已经有序
-
特点:
- 时间复杂度:
- 平均时间复杂度为O(n*logn)。在每次划分操作中,如果能够将数组比较均匀地分为两部分,那么总共需要进行次划分操作,每次划分操作需要遍历数组中的大部分元素,时间复杂度约为O(n),所以平均时间复杂度为O(n*logn)。
- 最坏情况时间复杂度为O(n*n),当数组已经有序或者逆序(对于选择第一个元素作为基准的情况)时,每次划分得到的两部分子数组长度相差很大,例如一个子数组长度为 n - 1,另一个子数组长度为 0,这样就会导致划分次数达到 n - 1 次,时间复杂度变为O(n*n)。
- 空间复杂度:
- 最好情况空间复杂度为O(logn),这是因为在快速排序的递归过程中,需要使用栈来保存每次划分的子数组信息,在平均情况下,递归深度为logn,所以空间复杂度为O(logn)。
- 最坏情况空间复杂度为O(n),当递归深度达到 n 时,例如数组已经有序的情况,需要占用较多的栈空间。
- 稳定性:快速排序是一种不稳定的排序算法。因为在划分过程中,交换元素的操作可能会改变相同元素的相对顺序。
- 时间复杂度:
-
主要步骤
-
/快速排序 // 三数取中 //GetMidIndex三数取中 校招一般不写 // 防止快速排序遇见有序情况是时间复杂度和直接插入的数量级一样 int GetMidIndex(int* a, int left, int right) { int mid = (left + right) >> 1; if (a[left] < a[mid]) { if (a[mid] < a[right]) { return mid; } else if (a[left] > a[right]) { return left; } else { return right; } } else // a[left] > a[mid] { if (a[mid] > a[right]) { return mid; } else if (a[left] < a[right]) { return left; } else { return right; } } }
GetMidIndex
函数实现了 “三数取中” 策略,目的是为了在快速排序算法中选择一个较合理的基准值(pivot),以避免在特殊情况下(如数组已基本有序)快速排序算法的性能退化到与直接插入排序相近的时间复杂度。 -
1.挖坑法
-
// 挖坑法 只写了一趟,若需完整算法,应在QuickSort调用或进行递归 int PartSort1(int* a, int left, int right) { int index = GetMidIndex(a, left, right); Swap(&a[left], &a[index]); int begin = left, end = right; int pivot = begin; int key = a[begin]; // O(N) while (begin < end) { // 右边找小,放到左边 while (begin < end && a[end] >= key) --end; // 小的放到左边的坑里,自己形成新的坑位 a[pivot] = a[end]; pivot = end; // 左边找大 while (begin < end && a[begin] <= key) ++begin; // 大的放到右边的坑里,自己形成新的坑位 a[pivot] = a[begin]; pivot = begin; } pivot = begin; a[pivot] = key; return pivot; }
-
选择基准值并进行交换:
int index = GetMidIndex(a, left, right);
调用 “三数取中” 函数GetMidIndex
选择一个相对中间的元素索引。Swap(&a[left], &a[index]);
将这个中间元素与数组最左端的元素交换,使得后续操作可以以a[left]
作为基准值进行划分。
-
初始化变量:
int begin = left, end = right;
分别定义了两个指针begin
和end
,分别指向数组的左端和右端。int pivot = begin;
定义了一个变量pivot
,表示当前的 “坑位” 索引,初始值为begin
。int key = a[begin];
将基准值存储在变量key
中。
-
划分过程:
while (begin < end)
循环用于进行划分操作,直到两个指针相遇。- 右边找小:
while (begin < end && a[end] >= key)
从右往左扫描,找到第一个小于基准值的元素。--end;
如果找到小于基准值的元素,将指针end
向左移动一位。a[pivot] = a[end];
将找到的小于基准值的元素放入当前的 “坑位”,同时更新 “坑位” 索引为end
。
- 左边找大:
while (begin < end && a[begin] <= key)
从左往右扫描,找到第一个大于基准值的元素。++begin;
如果找到大于基准值的元素,将指针begin
向右移动一位。a[pivot] = a[begin];
将找到的大于基准值的元素放入当前的 “坑位”,同时更新 “坑位” 索引为begin
。
-
2.左右指针
//左右指针:在挖坑法的基础上,begin找大,end找小 //只不过没有坑了,而是把左右指针换了 //只写了一趟,若需完整算法,应在QuickSort调用或进行递归 int PartSort2(int* a, int left, int right) { int index = GetMidIndex(a, left, right);//三数取中 Swap(&a[left], &a[index]);//永远把关键字放在最左边 int begin = left, end = right; int keyi = begin; while (begin < end) { // 找小 while (begin < end && a[end] >= a[keyi]) { --end; } // 找大 while (begin < end && a[begin] <= a[keyi]) { ++begin; } Swap(&a[begin], &a[end]); } Swap(&a[begin], &a[keyi]);//把之前找的关键字存在begin和end相遇的地方 return begin;//返回相遇的地方 }
-
选择基准值并进行交换:
int index = GetMidIndex(a, left, right);
调用 “三数取中” 函数选择一个相对中间的元素索引。Swap(&a[left], &a[index]);
将中间元素与数组最左端的元素交换,这样后续操作可以以a[left]
作为基准值进行划分。
-
初始化变量:
int begin = left, end = right;
定义两个指针分别指向数组的左端和右端。int keyi = begin;
将基准值的索引初始化为begin
。
-
划分过程:
while (begin < end)
循环用于进行划分操作,直到两个指针相遇。- 找小:
while (begin < end && a[end] >= a[keyi])
从右往左扫描,找到第一个小于基准值的元素。如果当前元素大于等于基准值,继续向左移动指针end
。--end;
当找到小于基准值的元素时,将指针end
向左移动一位。
- 找大:
while (begin < end && a[begin] <= a[keyi])
从左往右扫描,找到第一个大于基准值的元素。如果当前元素小于等于基准值,继续向右移动指针begin
。++begin;
当找到大于基准值的元素时,将指针begin
向右移动一位。
Swap(&a[begin], &a[end]);
当找到一 “小” 一 “大” 两个元素后,交换它们的位置。
-
确定基准值的最终位置并返回:
Swap(&a[begin], &a[keyi]);
当左右指针相遇时,将基准值与相遇位置的元素交换,确定基准值的最终位置。return begin;
返回相遇位置的索引,即基准值在排序后的位置。
-
3.前后指针
//前后指针法:cur找小,每次遇到比key小的值,就停下来,++prev //然后交换cur和prev位置的值 //核心思想就是把小的值往前移 int PartSort3(int* a, int left, int right) { int index = GetMidIndex(a, left, right);//三数取中 Swap(&a[left], &a[index]); int keyi = left;//永远把关键字放在最左边 int prev = left, cur = left + 1;//形成前后指针 while (cur <= right) { if (a[cur] < a[keyi] && ++prev != cur)//如果++prev和cur相等,不用换,提高效率 { Swap(&a[prev], &a[cur]); } ++cur;//不论是否小于,cur都要往前移,所以cur和prev也许中间隔了好几个大的数 } Swap(&a[keyi], &a[prev]);//把关键字放在此时prev的位置 return prev; }
-
选择基准值并进行交换:
int index = GetMidIndex(a, left, right);
调用 “三数取中” 函数选取相对中间的元素索引。Swap(&a[left], &a[index]);
将中间元素与数组最左端的元素交换,后续以a[left]
作为基准值进行划分。
-
初始化变量:
int keyi = left;
将基准值的索引设置为left
,表示基准值在数组的最左端。int prev = left, cur = left + 1;
初始化前后指针,prev
初始指向基准值,cur
指向基准值的下一个位置。
-
划分过程:
while (cur <= right)
循环用于遍历数组,进行划分操作。if (a[cur] < a[keyi] && ++prev!= cur)
如果当前元素小于基准值,且prev
自增后不等于cur
,说明需要交换。交换a[prev]
和a[cur]
,这样就将小于基准值的元素移动到了左边。++cur;
无论当前元素是否小于基准值,cur
都要向前移动,继续检查下一个元素。
-
确定基准值的最终位置并返回:
Swap(&a[keyi], &a[prev]);
循环结束后,将基准值与prev
位置的元素交换,确定基准值在排序后的位置。return prev;
返回基准值的最终位置索引。