冒泡排序
原理讲解:
冒泡排序(Bubble Sort)是一种简单的排序算法。它的基本思想是重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
冒泡排序的工作原理如下:
- 比较和交换:
- 比较相邻的两个元素,如果它们的顺序错误,就交换它们的位置。
- 遍历数列的所有元素,重复比较和交换的过程。
- 重复:
- 重复上述过程,但是每次遍历都会发现更少的元素需要交换,因为它们已经按顺序排列了。
- 结束条件:
- 当数列已经完全有序时,停止排序。
冒泡排序的时间复杂度是O(n^2),因为它的两层嵌套循环:外层循环遍历所有元素,内层循环在每个元素的位置上进行比较和交换。在最坏的情况下,即数列完全逆序时,冒泡排序的时间复杂度达到O(n^2)。
冒泡排序的空间复杂度是O(1),因为它不需要额外的存储空间,只需要常数级别的额外空间来存储临时变量。
冒泡排序的优点是它的实现简单,而且它是原地排序(即只需用到O(1)的额外空间的排序)。它的缺点是当数据量较大时,性能较差。
深度讲解:
让我们通过一个数学例子来解释冒泡排序的工作原理。假设我们有一个数列:[64, 34, 25, 12, 22, 11, 90]
,我们想要对其进行升序排序。
- 第一次遍历:
- 比较第一个元素
64
和第二个元素34
,发现顺序错误,交换它们的位置。 - 继续比较第二个元素
34
和第三个元素25
,交换它们的位置。 - 继续比较第三个元素
25
和第四个元素12
,交换它们的位置。 - 继续比较第四个元素
12
和第五个元素22
,交换它们的位置。 - 继续比较第五个元素
22
和第六个元素11
,交换它们的位置。 - 继续比较第六个元素
11
和第七个元素90
,发现顺序正确,无需交换。 - 第一次遍历后,最大的元素
90
已经移到了数列的末尾。
- 比较第一个元素
- 第二次遍历:
- 重复第一次遍历的过程,但是这次只遍历前六个元素。
- 比较第一个元素
64
和第二个元素34
,发现顺序错误,交换它们的位置。 - 继续比较第二个元素
34
和第三个元素25
,交换它们的位置。 - 继续比较第三个元素
25
和第四个元素12
,交换它们的位置。 - 继续比较第四个元素
12
和第五个元素22
,交换它们的位置。 - 继续比较第五个元素
22
和第六个元素11
,交换它们的位置。 - 第二次遍历后,第二大的元素
64
已经移到了数列的倒数第二个位置。
- 重复:
- 重复上述过程,每次遍历都会发现更少的元素需要交换,因为它们已经按顺序排列了。
- 经过多次遍历,直到整个序列都是有序的。
最终,我们得到了一个完全有序的数列:[11, 12, 22, 25, 34, 64, 90]
。
这个例子展示了冒泡排序的每一步骤。每次我们比较相邻的两个元素,如果它们的顺序错误,就交换它们的位置。这个过程会重复,直到整个序列都是有序的。
代码示例:
include
void bubbleSort(int arr[], int n) {
int i, j;
bool swapped;
for (i = 0; i < n - 1; i++) {
swapped = false; // 假设这一轮没有元素需要交换
for (j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true; // 标记有元素交换
}
}
// 如果这一轮没有元素需要交换,数列已经是有序的,可以提前结束
if (!swapped)
break;
}
}
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr) / sizeof(arr[0]);
printf("Original array: \n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
bubbleSort(arr, n);
printf("Sorted array: \n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
选择排序
原理讲解:
选择排序(Selection Sort)是一种简单的排序算法。它的基本思想是:首先在未排序的序列中找到最小(或最大)的元素,然后将其与未排序序列的第一个元素交换位置;接着在剩下的未排序序列中找到最小(或最大)的元素,并将其与未排序序列的第二个元素交换位置;重复这个过程,直到整个序列有序。
选择排序的步骤如下:
遍历:在未排序的序列中找到最小(或最大)的元素。
交换:将找到的最小(或最大)元素与未排序序列的第一个元素交换位置。
缩小范围:此时,未排序序列的第一个元素已经是有序的,所以将未排序序列的范围缩小到第二个元素到最后一个元素,重复步骤1和2。
重复:重复步骤1-3,直到整个序列有序。
选择排序的时间复杂度是O(n^2),因为它需要两层嵌套循环:外层循环遍历所有未排序的元素,内层循环在每个元素的位置上找到最小(或最大)元素。在最坏的情况下,即每次内层循环都找到最后一个元素,选择排序的时间复杂度达到O(n^2)。
选择排序的空间复杂度是O(1),因为它只需要常数级别的额外空间来存储临时变量。
选择排序的优点是它的实现简单,而且它是原地排序(即只需用到O(1)的额外空间的排序)。它的缺点是当数据量较大时,性能较差。
深度讲解:
让我们通过一个数学例子来解释选择排序的工作原理。假设我们有一个数列:[12, 11, 13, 5, 6, 7]
,我们想要对其进行升序排序。
- 第一次遍历:
- 在未排序的序列中找到最小的元素,这里是
5
。 - 将
5
与未排序序列的第一个元素12
交换位置,得到新的序列[5, 11, 13, 12, 6, 7]
。
- 在未排序的序列中找到最小的元素,这里是
- 第二次遍历:
- 在未排序的序列中找到最小的元素,这里还是
5
,因为它已经是最小的了。 - 将
5
与未排序序列的第二个元素11
交换位置,得到新的序列[5, 5, 13, 12, 6, 7]
。
- 在未排序的序列中找到最小的元素,这里还是
- 第三次遍历:
- 在未排序的序列中找到最小的元素,这里是
6
。 - 将
6
与未排序序列的第三个元素13
交换位置,得到新的序列[5, 5, 6, 12, 13, 7]
。
- 在未排序的序列中找到最小的元素,这里是
- 第四次遍历:
- 在未排序的序列中找到最小的元素,这里是
7
。 - 将
7
与未排序序列的第四个元素12
交换位置,得到新的序列[5, 5, 6, 7, 13, 12]
。
- 在未排序的序列中找到最小的元素,这里是
- 第五次遍历:
- 在未排序的序列中找到最小的元素,这里是
12
。 - 将
12
与未排序序列的第五个元素13
交换位置,得到新的序列[5, 5, 6, 7, 12, 13]
。
- 在未排序的序列中找到最小的元素,这里是
- 第六次遍历:
- 在未排序的序列中找到最小的元素,这里是
13
。 13
已经是未排序序列中的最大元素,不需要交换。
最终,我们得到了一个完全有序的数列:[5, 5, 6, 7, 12, 13]
。
- 在未排序的序列中找到最小的元素,这里是
代码示例:
include
void selectionSort(int arr[], int n) {
int i, j, min_idx;
// 外层循环遍历所有未排序的元素
for (i = 0; i < n - 1; i++) {
// 假设最小元素是当前外层循环的第一个元素
min_idx = i;
// 内层循环遍历剩余未排序的元素,找到最小元素
for (j = i + 1; j < n; j++) {
if (arr[j] < arr[min_idx]) {
min_idx = j;
}
}
// 将找到的最小元素与外层循环的第一个元素交换位置
if (min_idx != i) {
int temp = arr[i];
arr[i] = arr[min_idx];
arr[min_idx] = temp;
}
}
}
int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int n = sizeof(arr) / sizeof(arr[0]);
printf("Original array: \n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
selectionSort(arr, n);
printf("Sorted array: \n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
插入排序
原理讲解:
插入排序(Insertion Sort)是一种简单的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常使用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
插入排序的工作原理如下:
- 初始化:数组的前一个元素视为有序序列,初始时只有一个元素,因此它是已排序的。
- 插入:对于数组中的每一个元素,从已排序的序列中找到它的位置并插入。
- 重复:重复步骤2,直到所有元素都被插入到已排序的序列中。
插入排序的时间复杂度是O(n^2),因为它需要两层循环:外层循环遍历数组中的每个元素,内层循环将元素插入到已排序序列中的正确位置。在最坏的情况下,即数组完全逆序时,插入排序的时间复杂度达到O(n^2)。
插入排序的空间复杂度是O(1),因为它不需要额外的存储空间,只需要常数级别的额外空间来存储临时变量。
插入排序的优点是它适用于小规模数据或基本有序的数据。它的缺点是当数据量较大时,性能较差。
深度讲解:
让我们通过一个数学例子来解释插入排序的工作原理。假设我们有一个数列:[12, 11, 13, 5, 6, 7]
,我们想要对其进行升序排序。
- 初始化:数列的前一个元素被视为有序序列,初始时只有一个元素,因此它是已排序的。
- 插入:对于数列中的每个元素,我们从已排序的序列中找到它的位置并插入。
- 第一步:我们有一个已排序的序列
[12]
和一个未排序的序列[11, 13, 5, 6, 7]
。我们从未排序序列中取出第一个元素11
,它小于已排序序列的最后一个元素12
,所以我们不需要做任何移动,已排序序列仍然是[12]
,未排序序列变为[11, 13, 5, 6, 7]
。
- 第二步:我们再次取出未排序序列中的第一个元素
13
,它大于已排序序列的最后一个元素12
,所以我们将13
放在12
的后面,已排序序列变为[12, 13]
,未排序序列变为[11, 5, 6, 7]
。
- 第三步:我们取出未排序序列中的第一个元素
5
,它小于已排序序列的最后一个元素13
,所以我们从已排序序列的末尾开始,将大于5
的元素向后移动,直到找到5
的位置,然后将5
插入到这个位置。已排序序列变为[12, 13, 5]
,未排序序列变为[11, 6, 7]
。
- 第四步:我们取出未排序序列中的第一个元素
6
,它小于已排序序列的最后一个元素5
,所以我们从已排序序列的末尾开始,将大于6
的元素向后移动,直到找到6
的位置,然后将6
插入到这个位置。已排序序列变为[12, 13, 5, 6]
,未排序序列变为[11, 7]
。
- 第五步:我们取出未排序序列中的第一个元素
7
,它大于已排序序列的最后一个元素6
,所以我们将7
放在6
的后面,已排序序列变为[12, 13, 5, 6, 7]
,未排序序列变为[11]
。
- 重复:我们重复上述步骤,直到所有元素都被插入到已排序的序列中。
最终,我们得到了一个完全有序的数列:[5, 6, 7, 11, 12, 13]
。
代码示例:
include
void insertionSort(int arr[], int n) {
int i, key, j;
for (i = 1; i < n; i++) {
key = arr[i];
j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key;
}
}
int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int n = sizeof(arr) / sizeof(arr[0]);
printf("Original array: \n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
insertionSort(arr, n);
printf("Sorted array: \n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
希尔排序
原理
希尔排序(Shell Sort)是一种改进的插入排序算法,由Donald Shell于1959年提出。
它的基本思想是将整个待排序的序列分割成若干个子序列,分别进行直接插入排序,待整个序列中的记录基本有序时,再对全体记录进行一次直接插入排序。
希尔排序的原理可以概括为以下几点:
- 选择间隔(gap):希尔排序首先选择一个间隔序列
gap1, gap2, ..., gapk
,其中gap1 > gap2 > ... > gapk > 1
,
最后一个间隔 gapk
通常为1。不同的间隔序列可能会影响算法的性能。
- 分组排序:根据当前间隔
gap
,将序列分成gap
个子序列。
每个子序列包含间隔为 gap
的元素。对每个子序列使用插入排序。
- 逐步缩小间隔:完成一轮排序后,缩小间隔
gap
,通常是取gap = gap / 2
,
然后对新的子序列进行插入排序。
- 最终排序:当间隔缩小到1时,整个序列基本有序,此时进行最后一轮插入排序,
由于序列已经接近有序,所以这一轮排序会非常快。
希尔排序的优点在于,它通过预排序使得数据更加紧凑,减少了插入排序时数据移动的次数,从而提高了效率。与简单插入排序相比,希尔排序对于中等大小的数组排序速度更快。
需要注意的是,希尔排序的时间复杂度取决于选择的间隔序列,不同的间隔序列会导致不同的性能。至今为止,没有找到最好的间隔序列,但即使使用简单的间隔序列,希尔排序通常也比简单插入排序要快。
讲解
当然可以。让我们通过一个具体的例子来展示希尔排序的过程。
假设我们有一个数组:[35, 33, 42, 10, 14, 19, 27, 44]
,并使用希尔排序对其进行排序。我们可以选择一个简单的间隔序列,比如 gap = n/2
,其中 n
是数组的长度,并且在每一轮排序后 gap
都减半。
首先,我们计算初始的 gap
,这里 n = 8
,所以 gap = 8 / 2 = 4
。
第一轮排序:
- 我们将数组分为
gap
个子序列:[35, 14]
,[33, 19]
,[42, 27]
,[10, 44]
。
- 对每个子序列进行插入排序:
[14, 35]
,[19, 33]
,[27, 42]
,[10, 44]
。
- 合并子序列得到:
[14, 19, 27, 10, 35, 33, 42, 44]
。
现在,我们将 gap
减半,gap = 4 / 2 = 2
。
第二轮排序:
- 我们将数组分为
gap
个子序列:[14, 27]
,[19, 42]
,[35, 44]
,[33, 10]
。
- 对每个子序列进行插入排序:
[14, 27]
,[19, 42]
,[35, 44]
,[10, 33]
。
- 合并子序列得到:
[14, 19, 35, 10, 27, 42, 33, 44]
。
再次将 gap
减半,gap = 2 / 2 = 1
。
最后一轮排序:
- 此时,
gap = 1
,整个数组被视为一个子序列。
- 对整个数组进行插入排序:
[14, 19, 10, 27, 35, 33, 42, 44]
->[10, 14, 19, 27, 33, 35, 42, 44]
。
现在数组已经完全有序,排序完成。
这个例子展示了希尔排序如何通过间隔序列来改善插入排序的性能。通过预先将数组变成部分有序,希尔排序减少了插入排序过程中需要的比较和交换次数。
代码示例
include
void shellSort(int arr[], int n) {
for (int gap = n / 2; gap > 0; gap /= 2) {
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j;
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = temp;
}
}
}
int main() {
int arr[] = {35, 33, 42, 10, 14, 19, 27, 44};
int n = sizeof(arr) / sizeof(arr[0]);
printf("Original array: \n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
shellSort(arr, n);
printf("Sorted array: \n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
这段代码定义了一个shellSort函数,它接受一个整数数组arr和数组的长度n。在main函数中,我们定义了一个示例数组,调用shellSort对其进行排序,并打印排序前后的数组。
shellSort函数首先确定初始的间隔gap,然后进行多轮排序,每轮排序后gap减半,直到gap为1,此时进行最后一轮插入排序。在每一轮排序中,我们使用插入排序的思想,但是插入排序的比较和交换是间隔gap的元素。
运行这段代码,你将看到如下输出:
Original array:
35 33 42 10 14 19 27 44
Sorted array:
10 14 19 27 33 35 42 44
归并排序
原理
归并排序(Merge Sort)是一种分治算法,其思想是将原始数组切分成较小的数组,直到每个小数组只有一个位置,然后将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。由于是递归操作,归并排序保证了每个小数组都是有序的,它是稳定的排序算法。
归并排序的主要步骤如下:
分解(Divide):将数组分解成两半,递归地对两个子数组进行分解,直到每个子数组只有一个元素。
归并(Conquer):从最小的子数组开始,两两归并成较大的有序数组。归并过程中,需要额外的存储空间来存储归并后的数组。
合并(Combine):将归并后的数组复制回原始数组的位置,完成排序。
归并排序的时间复杂度是O(n log n),无论是最好、最坏还是平均情况,因为分解数组的时间复杂度是O(log n),归并操作的时间复杂度是O(n)。
讲解
让我们通过一个简单的数学例子来解释归并排序算法的工作原理。
假设我们有一个数列:[38, 27, 43, 3, 9, 82, 10]
,我们想要对它进行排序。
- 分解(Divide)阶段:
- 我们首先将数列分解成两半:
[38, 27, 43, 3]
和[9, 82, 10]
。
- 我们首先将数列分解成两半:
- 然后继续分解:
[38, 27]
,[43, 3]
,[9, 82]
,和[10]
。
- 再继续分解:
[38]
,[27]
,[43]
,[3]
,[9]
,[82]
,和[10]
。
- 归并(Conquer)阶段:
- 现在,我们开始归并。首先归并分解出来的单个元素:
[27, 38]
,[3, 43]
,[9, 82]
,和[10]
。
- 现在,我们开始归并。首先归并分解出来的单个元素:
- 然后,我们继续归并得到的有序数组:
[3, 27, 38, 43]
和[9, 10, 82]
。
- 合并(Combine)阶段:
- 最后,我们将两个有序数组归并成一个:
[3, 9, 10, 27, 38, 43, 82]
。
- 最后,我们将两个有序数组归并成一个:
这样,我们就得到了一个有序的数列。这个过程是递归进行的,每一层的归并都是基于它下一层的结果。
归并排序的关键在于归并步骤,它将两个有序数组合并成一个有序数组。这个过程中,我们需要额外的存储空间来暂存归并的结果,但是在算法结束后,这个额外的空间就被释放了。
归并排序的时间复杂度是O(n log n),因为它将数组分成两半log n次,每次归并需要O(n)的时间。归并排序的一个优点是它能够保证在最坏的情况下也有O(n log n)的时间复杂度,而且它是稳定的排序算法,即相同值的元素在排序后保持原有的顺序。
在归并排序中 又分为迭代法和递归法 这两者有一定的区别
区别
归并排序可以通过递归或迭代两种方式实现。它们的主要区别在于如何处理归并过程。
递归法:
自顶向下:递归法通常采用自顶向下的策略,首先将数组分割成越来越小的部分,直到每个部分只有一个元素,然后开始两两归并。
递归调用:递归法使用函数自身调用自身的方式来处理子问题,即递归地应用归并排序到子数组上。
代码简单:递归法的代码通常更简洁,更容易理解和实现。
深度优先:递归法在处理过程中采用深度优先的策略,先处理子问题,再合并结果。
迭代法:
自底向上:迭代法通常采用自底向上的策略,首先将数组中的每个元素看作一个有序的子数组,然后逐步将它们两两归并成更大的有序数组。
循环实现:迭代法使用循环而不是递归来实现归并过程,通常需要额外的空间来存储归并过程中的临时数组。
代码复杂:迭代法的代码通常比递归法复杂,因为需要手动管理数组的分割和归并过程。
广度优先:迭代法在处理过程中采用广度优先的策略,先归并较小的子数组,再逐步扩大归并的子数组规模。
深度讲解
让我们通过一个数学例子来比较递归法和迭代法在归并排序中的应用。
假设我们有一个数列:[38, 27, 43, 3, 9, 82, 10]
。
递归法:
- 分解阶段:
- 第一轮分解:
[38, 27, 43, 3]
和[9, 82, 10]
- 第一轮分解:
- 第二轮分解:
[38, 27]
,[43, 3]
,[9, 82]
,和[10]
- 第三轮分解:
[38]
,[27]
,[43]
,[3]
,[9]
,[82]
,和[10]
- 归并阶段:
- 第一轮归并:
[27, 38]
,[3, 43]
,[9, 82]
,和[10]
- 第一轮归并:
- 第二轮归并:
[3, 27, 38, 43]
和[9, 10, 82]
- 第三轮归并:
[3, 9, 10, 27, 38, 43, 82]
迭代法:
- 初始阶段:
- 将数列视为长度为1的有序子数列:
[38]
,[27]
,[43]
,[3]
,[9]
,[82]
,和[10]
- 将数列视为长度为1的有序子数列:
- 归并阶段:
- 第一轮归并:
[27, 38]
,[3, 43]
,[9, 82]
,和[10]
- 第一轮归并:
- 第二轮归并:
[3, 27, 38, 43]
和[9, 10, 82]
- 第三轮归并:
[3, 9, 10, 27, 38, 43, 82]
在递归法中,我们首先将数列分解到最小单元,然后再逐步归并。而在迭代法中,我们首先将数列视为最小单元,然后逐步增大归并单元的长度,直到整个数列被归并成一个有序数列。
两种方法的最终结果是一样的,但是它们的处理过程不同。递归法通过函数调用栈来管理子数组的归并过程,而迭代法使用循环和辅助数组来手动管理归并过程。
代码例子
递归法:
function mergeSort(arr):
if length of arr <= 1:
return arr
middle = length of arr / 2
left = mergeSort(first half of arr)
right = mergeSort(second half of arr)
return merge(left, right)
function merge(left, right):
result = empty array
while left and right are not empty:
if first element of left < first element of right:
add first element of left to result
remove first element of left
else:
add first element of right to result
remove first element of right
append remaining elements of left to result
append remaining elements of right to result
return result
排序法
function mergeSort(arr):
size = length of arr
temp = empty array of size size
for subSize in [1, 2, 4, ..., size]:
for start in [0, subSize, 2 subSize, ..., size - subSize]:
middle = start + subSize - 1
end = min(start + 2 subSize - 1, size - 1)
merge(arr, start, middle, end, temp)
return arr
function merge(arr, start, middle, end, temp):
left = start
right = middle + 1
index = start
while left <= middle and right <= end:
if arr[left] < arr[right]:
temp[index] = arr[left]
left = left + 1
else:
temp[index] = arr[right]
right = right + 1
index = index + 1
copy remaining elements from left subarray to temp
copy remaining elements from right subarray to temp
copy merged elements from temp back to original array
快速排序
原理
快速排序(Quick Sort)是一种高效的排序算法,它采用分治策略(Divide and Conquer)来对一系列数据进行排序。
快速排序的平均时间复杂度为O(n log n),在最坏情况下的时间复杂度为O(n^2),
但通常情况下它的性能要优于其他O(n log n)算法,因为它避免了大量的比较和交换操作。
快速排序的主要步骤如下:
- 选择基准值(Pivot):从数列中选取一个元素作为基准值。这个基准值是用来比较其他元素的,并且最终将数列分成两部分。
- 分区(Partitioning):重新排列数列,将所有比基准值小的元素移动到基准的左边,将所有比基准值大的元素移动到基准的右边。这个步骤完成后,基准元素就位于数列的中间位置,这个位置被称为分区点(Partition Point)。
- 递归排序(Recursively sort):递归地对基准左侧的子数列和右侧的子数列重复上述步骤,直到子数列的长度为0或1,这时子数列自然是有序的。
快速排序的关键在于分区操作,它能够有效地将数列分割成独立的两部分,从而减少比较和交换的次数。分区操作的效率直接影响到快速排序的性能。
快速排序的优点是它的原地排序(In-place Sorting)特性和平均情况下的高效性能。它的缺点是在最坏情况下(输入序列已经是有序的或者完全逆序的)性能会下降到O(n^2),尽管这种情况在实际应用中不常见。此外,快速排序需要一个额外的栈空间来存储递归调用的信息。
快速排序是一种不稳定的排序算法,也就是说,相等的元素在排序后可能会改变它们的相对顺序。然而,它的变体可以实现对稳定排序的要求。
深度讲解
让我们通过一个数学例子来展示快速排序的过程。假设我们有一个数列:[10, 7, 8, 9, 1, 5]
,我们将使用快速排序对其进行升序排序。
- 选择基准值(Pivot):
- 我们可以选择数列的第一个元素作为基准值,即
10
。
- 分区(Partitioning):
- 我们将数列中所有比基准值小的元素移动到基准的左边,所有比基准值大的元素移动到基准的右边。在这个例子中,
10
是基准值,所以数列变为[7, 8, 9, 1, 5, 10]
。现在,基准值10
位于数列的最后一个位置,而所有比它小的元素都在它左边。
- 递归排序(Recursively sort):
- 现在,我们需要对基准值左侧的子数列
[7, 8, 9, 1, 5]
和右侧的子数列(在这个例子中是空的,因为10
是最大的元素)进行排序。
- 对左侧子数列进行排序,我们选择
7
作为基准值,数列变为[5, 1, 7, 8, 9]
。基准值7
现在位于正确的位置,我们继续对[5, 1]
和[8, 9]
进行排序。
- 对
[5, 1]
进行排序,选择5
作为基准值,数列变为[1, 5]
。基准值5
位于正确的位置,我们不需要再对[1]
进行排序,因为它已经是有序的。
- 对
[8, 9]
进行排序,选择8
作为基准值,数列已经是正确的顺序[8, 9]
。
最终,我们得到了一个完全有序的数列:[1, 5, 7, 8, 9, 10]
。
这个例子展示了快速排序的每一步骤。请注意,快速排序的实际实现可能会有所不同,例如选择基准值的方法可能会有变化,但基本原理是相同的。
PS:有一个小小的问题,那就是这个基准值是随机的吗?聪明的你仔细想想
嘿嘿 仔细想想就知道 这个基准值可以随机也可以不随机
在快速排序中,选择基准值(Pivot)的方法有多种,可以是随机的,也可以是固定的。不同的选择方法可能会影响算法的性能,尤其是在数据分布不均匀的情况下。
- 随机选择:从数列中随机选择一个元素作为基准值。这种方法可以减少遇到最坏情况的可能性,因为它避免了总是选择最小或最大元素作为基准值。
- 固定选择:通常选择数列的第一个元素或最后一个元素作为基准值。这是最简单的选择方法,但可能会导致最坏情况性能,尤其是当数列已经是有序或接近有序时。
- 中位数选择:选择数列的中位数作为基准值。这种方法通常需要先对数列进行部分排序来找到中位数,这可能会增加额外的计算成本。
在实际应用中,随机选择基准值是一种比较流行的策略,因为它在大多数情况下都能提供良好的性能,并且实现起来相对简单。然而,即使是随机选择,也不能完全避免最坏情况的发生,因为随机性意味着仍然有可能选中最小或最大的元素作为基准值。
选择基准值的策略可以根据具体的应用场景和数据特性来决定。在某些情况下,如果已知数据分布的特性,可能会选择一种更适合特定情况的基准值选择方法。
深度讲解
让我们通过一个数学例子来比较递归法和迭代法在快速排序中的应用。
假设我们有一个数列:[10, 7, 8, 9, 1, 5]
。
递归法:
- 选择基准值(Pivot):
- 我们可以选择数列的第一个元素作为基准值,即
10
。
- 我们可以选择数列的第一个元素作为基准值,即
- 分区(Partitioning):
- 我们将数列中所有比基准值小的元素移动到基准的左边,所有比基准值大的元素移动到基准的右边。在这个例子中,
10
是基准值,所以数列变为[7, 8, 9, 1, 5, 10]
。现在,基准值10
位于数列的最后一个位置,而所有比它小的元素都在它左边。
- 我们将数列中所有比基准值小的元素移动到基准的左边,所有比基准值大的元素移动到基准的右边。在这个例子中,
- 递归排序(Recursively sort):
- 现在,我们需要对基准值左侧的子数列
[7, 8, 9, 1, 5]
和右侧的子数列(在这个例子中是空的,因为10
是最大的元素)进行排序。
- 现在,我们需要对基准值左侧的子数列
- 对左侧子数列进行排序,我们选择
7
作为基准值,数列变为[5, 1, 7, 8, 9]
。基准值7
现在位于正确的位置,我们继续对[5, 1]
和[8, 9]
进行排序。
- 对
[5, 1]
进行排序,选择5
作为基准值,数列变为[1, 5]
。基准值5
位于正确的位置,我们不需要再对[1]
进行排序,因为它已经是有序的。
- 对
[8, 9]
进行排序,选择8
作为基准值,数列已经是正确的顺序[8, 9]
。
最终,我们得到了一个完全有序的数列:[1, 5, 7, 8, 9, 10]
。
迭代法:
- 初始阶段:
- 将数列视为长度为1的有序子数列:
[10]
,[7]
,[8]
,[9]
,[1]
,和[5]
。
- 将数列视为长度为1的有序子数列:
- 归并阶段:
- 首先,我们将相邻的子数列进行归并:
[7, 10]
,[8, 9]
,和[1, 5]
。
- 首先,我们将相邻的子数列进行归并:
- 然后,我们继续归并得到的有序数组:
[1, 7, 10]
,[5, 8, 9]
。- 最终归并:
- 最后,我们将两个有序数组归并成一个:
[1, 5, 7, 8, 9, 10]
。
在递归法中,我们首先将数列分解到最小单元,然后再逐步归并。而在迭代法中,我们首先将数列视为最小单元,然后逐步增大归并单元的长度,直到整个数列被归并成一个有序数列。
两种方法的最终结果是一样的,但是它们的处理过程不同。递归法通过函数调用栈来管理子数组的归并过程,而迭代法使用循环和辅助数组来手动管理归并过程。
代码示例
递归法:
function quickSort(arr, low, high):
if low < high:
pi = partition(arr, low, high)
quickSort(arr, low, pi - 1)
quickSort(arr, pi + 1, high)
迭代法:
function quickSort(arr):
stack = empty stack
push (low, high) onto stack
while stack is not empty:
pop (low, high) from stack
pi = partition(arr, low, high)
push (low, pi - 1) onto stack
push (pi + 1, high) onto stack