前言:排序算法在计算机科学中占有重要地位,不同的算法适用于不同的场景。本文将深入探讨快速排序、归并排序和计数排序。
✨✨✨这里是秋刀鱼不做梦的BLOG
✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客
先让我们看一下本文大致的讲解内容:
对于每个排序算法,我们都会从算法简介、其原理与步骤、代码实现、时间复杂度分析、空间复杂度分析与该排序算法的应用场景这几个方面来进行讲解。
目录
1.快速排序 (Quicksort)
(1)算法简介
快速排序是一种高效的排序算法,由C.A.R. Hoare在1960年提出。它采用分治法(Divide and Conquer),通过递归地将未排序的部分分割为较小的子数组进行排序,再将其合并。快速排序的平均时间复杂度为 O(nlogn),在大多数情况下比其他 O(nlogn) 的算法,如归并排序,具有更好的性能。
当然,我相信读者读完快速排序的简介之后,对于快速排序算法的认识还是没什么认识的,接下来让我们直接带着你边学如何实现排序算法边理解该算法的内核。
(2)算法的原理与步骤
快速排序的核心思想是选择一个“基准”(pivot),并通过一系列的比较和交换操作将数组分成两部分:一部分所有元素小于基准,另一部分所有元素大于基准。然后,对这两部分分别递归地应用快速排序。具体步骤如下:
-
选择基准: 从数组中选择一个元素作为基准。选择基准的方法有多种,如选择第一个元素、最后一个元素、随机选择或三数取中法。
-
分区 (Partitioning): 通过遍历数组,将所有小于基准的元素放在基准的左边,所有大于基准的元素放在右边。
-
递归排序: 对基准左边和右边的子数组递归地进行快速排序。
-
组合: 由于分区已经确保了基准左边的元素小于基准,右边的元素大于基准,因此当递归完成后,数组已经是有序的。
——看完了上述大体如何实现快速排序算法之后,让我们从代码的层面来实现一下快速排序算法。
(3)Java代码实现
以下为Java中实现快速排序的大致代码:
public void quickSort(int[] array) {
int end = array.length - 1;
quickSort(array, 0, end);
}
private void quickSortHo(int[] array, int left, int right) {
// 如果子数组长度为1或更小,则返回(即递归终止条件)
if (left >= right) {
return;
}
// 查找基准元素的位置,并将数组分成两部分
int target = findTarget(array, left, right);
// 对基准元素左边的子数组进行递归排序
quickSort(array, left, target - 1);
// 对基准元素右边的子数组进行递归排序
quickSort(array, target + 1, right);
}
从上述代码中我们可以看到我们在寻找基准元素位置的时候,使用了findTarget()方法,那么这个方法该如何实现呢?
实现该方法的方式大致有三种,分别是:Hoare法、挖坑法和前后指针法。接下来我们一一进行讲解。
【1】Hoare法
public int findTargetH(int[] array, int left, int right) {
// 获取中间索引并将中间元素交换到数组的最左端
int mid = middleIndex(array, left, right);
swap(array, left, mid);
// 将最左端的元素作为基准元素
int temp = array[left];
int keyi = left; // 保存基准元素的初始位置
// 开始从数组的两端向中间扫描
while (left < right) {
// 从右向左扫描,找到第一个小于基准的元素
while (left < right && array[right] >= temp) {
right--;
}
// 从左向右扫描,找到第一个大于基准的元素
while (left < right && array[left] <= temp) {
left++;
}
// 交换左侧找到的大于基准的元素和右侧找到的小于基准的元素
swap(array, left, right);
}
// 将基准元素放置到最终的位置,即左、右指针相遇的位置
swap(array, keyi, left);
// 返回基准元素的位置索引
return left;
}
解释说明:
middleIndex(array, left, right)
:假设这是一个用于计算数组中间索引的方法,以获取中间元素的位置。
swap(array, left, mid)
:将中间元素和最左端元素交换,使得基准元素(最左端元素)在处理前放到适当的位置。
temp
:基准元素,用于比较其他元素。
keyi
:保存基准元素的初始索引位置,以便最终将基准元素放到正确的位置。
while (left < right)
:循环扫描数组,直到左右指针相遇。
swap(array, left, right)
:当找到左侧大于基准的元素和右侧小于基准的元素时,交换它们的位置。最后一次
swap
:将基准元素放回到排序后应在的位置上,并返回该位置的索引。
——这就是Hoare实现查找基准元素位置。
【2】挖坑法
public int findTargetW(int[] array, int left, int right) {
// 将最左端的元素作为基准元素
int temp = array[left];
// 开始从数组的两端向中间扫描
while (left < right) {
// 从右向左扫描,找到第一个小于基准的元素
while (left < right && array[right] >= temp) {
right--;
}
// 将小于基准的元素移到左侧
array[left] = array[right];
// 从左向右扫描,找到第一个大于基准的元素
while (left < right && array[left] <= temp) {
left++;
}
// 将大于基准的元素移到右侧
array[right] = array[left];
}
// 将基准元素放置到最终的位置,即左、右指针相遇的位置
array[left] = temp;
// 返回基准元素的位置索引
return left;
}
解释说明:
temp
:基准元素,取自数组的最左端,用于比较和调整其他元素的位置。
while (left < right)
:主循环,通过左右指针的移动将数组分为小于基准和大于基准的两部分,直到左右指针相遇。
array[left] = array[right]
:将右侧扫描中找到的小于基准的元素移动到左侧。
array[right] = array[left]
:将左侧扫描中找到的大于基准的元素移动到右侧。最后一步:将基准元素放置在最终的正确位置,并返回该位置的索引,以供进一步的递归使用。
——这就是挖坑法实现查找基准元素位置。
【3】前后指针法
public int findTargetQ(int[] array, int left, int right) {
// 当前基准元素的位置(初始为左边界)
int cur = left;
// 从基准元素的下一个位置开始遍历
int prev = left + 1;
// 保存基准元素的值
int temp = array[left];
// 遍历数组,直到右边界
while (prev <= right) {
// 如果当前元素小于基准元素且 cur 与 prev 不相等,则交换这两个元素
if (array[prev] < temp && ++cur != prev) {
swap(array, cur, prev);
}
// 移动到下一个元素
prev++;
}
// 将基准元素放置到最终的位置,即 cur 的位置
swap(array, left, cur);
// 返回基准元素的位置索引
return cur;
}
解释说明:
cur
:表示当前已经确定位置的元素的索引,初始值为左边界,负责标记小于基准元素的部分的末尾。
prev
:用于遍历数组的索引,从基准元素的下一个位置开始。
temp
:基准元素的值,用于与其他元素进行比较。
while (prev <= right)
:遍历数组中的元素,寻找小于基准元素的值,并将其与当前元素进行交换。
if (array[prev] < temp && ++cur != prev)
:如果当前元素小于基准元素,并且cur
和prev
不相等(确保不进行自我交换),则交换这两个元素。
swap(array, cur, left)
:将基准元素放置到最终的位置,即所有小于基准元素的元素的末尾。
return cur
:返回基准元素的位置索引,用于进一步的排序。
——这就是前后指针法实现查找基准元素位置。
通过上述的学习,我们就了解了如何使用Java代码来实现快速排序算法。
(4)时间复杂度和空间复杂度
-
时间复杂度
-
平均时间复杂度: O(nlogn)。快速排序在分区步骤中,每次都将数组分成两个大致相等的部分,因此递归的深度为 O(logn),每层递归处理的元素数为 O(n)。
-
最坏时间复杂度: O(n2)。当每次分区产生的子数组大小极度不均匀(如数组已经有序或逆序)时,递归深度变为 O(n)。
-
-
空间复杂度: O(logn)(主要为递归调用栈的空间)。由于每次递归都会消耗栈空间,因此快速排序的空间复杂度与递归的深度相关。
(5)算法的应用场景
快速排序适用于大部分需要排序的场景,尤其是:
大数据集: 快速排序的平均性能优于其他排序算法,适合处理大规模数据集。
内存空间有限的情况: 相较于归并排序,快速排序的空间复杂度更低,更适合内存资源有限的场景。
不需要稳定排序的场景: 快速排序是一个不稳定的排序算法,不适用于需要保持相同元素相对顺序的场景。
这样我们就学习完了快速排序算法了!!!
2.归并排序 (Merge Sort)
(1)算法简介
归并排序是一种稳定的排序算法,采用分治策略,将待排序的数组分成若干子数组,分别对每个子数组进行排序,再将这些子数组合并成一个有序数组。归并排序的时间复杂度为 O(nlogn),在数据量较大且对排序稳定性要求较高的场景中有较好的表现。
同样,我们接下来带着你边学如何实现排序算法边理解该算法的内核。
(2)算法的原理与步骤
归并排序的基本思想是将数组分成尽可能小的子数组(每个子数组只有一个元素),然后逐步合并这些子数组,使得合并后的数组有序。具体步骤如下:
-
分割: 将数组分成两部分,递归地对每一部分进行归并排序。
-
排序并合并: 当每个子数组的长度为1时,开始合并相邻的子数组。在合并时,使用双指针技术,比较两个子数组的元素,将较小的元素放入临时数组中。
-
递归处理: 对左右子数组分别递归应用归并排序,直至最终将所有元素合并为一个有序数组。
——看完了上述大体如何实现归并排序算法之后,让我们从代码的层面来实现一下快速排序算法。
(3)Java代码实现
以下为Java中实现归并排序的大致代码:
public void mergeSort(int[] array) {
// 调用归并排序的递归方法,排序整个数组
mergeInsertSort(array, 0, array.length - 1);
}
public void mergeInsertSort(int[] array, int left, int right) {
// 如果子数组只有一个元素或为空,则返回(递归终止条件)
if (left >= right) {
return;
}
// 计算中间位置,将数组分成两部分
int mid = (left + right) / 2;
// 对左半部分进行递归排序
mergeInsertSort(array, left, mid);
// 对右半部分进行递归排序
mergeInsertSort(array, mid + 1, right);
// 合并两个已排序的子数组
merge(array, left, mid, right);
}
public void merge(int[] array, int left, int mid, int right) {
// 右半部分的起始位置
int rightBegin = mid + 1;
// 左半部分的起始位置
int leftBegin = left;
// 用于存放合并结果的临时数组
int i = 0;
int[] ret = new int[right - left + 1];
// 合并两个已排序的子数组
while (left <= mid && rightBegin <= right) {
// 比较左右两部分的元素,将较小的元素放入临时数组
if (array[left] < array[rightBegin]) {
ret[i++] = array[left++];
} else {
ret[i++] = array[rightBegin++];
}
}
// 将左半部分剩余的元素添加到临时数组中
while (left <= mid) {
ret[i++] = array[left++];
}
// 将右半部分剩余的元素添加到临时数组中
while (rightBegin <= right) {
ret[i++] = array[rightBegin++];
}
// 将临时数组中的元素复制回原数组的相应位置
for (int j = 0; j < ret.length; j++) {
array[j + leftBegin] = ret[j];
}
}
解释:
mergeSort(int[] array)
:这是归并排序的入口方法,它调用mergeInsertSort
方法对整个数组进行排序。
mergeInsertSort(int[] array, int left, int right)
:这是归并排序的递归方法,递归地将数组分成更小的部分并排序,然后调用merge
方法合并已排序的子数组。
merge(int[] array, int left, int mid, int right)
:这是合并两个已排序的子数组的方法。它创建一个临时数组ret
,将左右两部分按顺序合并到ret
中,然后将合并后的结果复制回原数组。
——这样我们就了解了如何使用Java代码来实现归并排序算法。
(4)时间复杂度和空间复杂度
-
时间复杂度: O(nlogn)。每次分割将数组对半分,深度为 O(logn),合并过程需要遍历整个数组,因此时间复杂度为 O(nlogn)。
-
空间复杂度: O(n)。归并排序需要额外的空间来存储临时数组,用于合并过程中临时存放子数组的元素。
(5)算法的应用场景
归并排序的稳定性和时间复杂度使其适用于以下场景:
稳定性要求高的排序: 归并排序是一种稳定排序算法,适用于对相同值的元素相对顺序有要求的场景。
外部排序: 归并排序适用于处理超大数据集的外部排序,由于其稳定性和性能,尤其适用于磁盘文件的排序操作。
数据集较大且未全部加载到内存: 在处理大规模数据时,归并排序由于其分割和合并的特性,可以有效地处理不能一次性加载到内存的数据。
——这样我们学习完了如何在Java中实现归并算法。
3.计数排序 (Counting Sort)
(1)算法简介
计数排序是一种非比较排序算法,主要用于对整数进行排序。它通过计算每个元素在数组中出现的次数来确定其在排序后数组中的位置。这种排序算法适用于元素范围较小且数据量较大的场景。
同样,我们接下来带着你边学如何实现排序算法边理解该算法的内核。
(2)算法的原理与步骤
计数排序的基本思想是创建一个计数数组,通过统计原始数组中每个元素的出现次数来确定它们在排序后数组中的正确位置。具体步骤如下:
-
计算最大值和最小值: 确定数组中的最大值和最小值,进而决定计数数组的大小。
-
计数: 创建一个计数数组,将每个元素出现的次数记录在计数数组中。
-
累加计数: 对计数数组中的值进行累加,以便确定每个元素的最终位置。
-
构建排序数组: 遍历原始数组,通过计数数组确定每个元素在排序后数组中的位置,构建最终的有序数组。
——接下来让我们使用代码来实现一下计数排序。
(3)Java代码实现
public void countSort(int[] array) {
// 初始化最小值和最大值为数组的第一个元素
int min = array[0];
int max = array[0];
// 遍历数组,找到最小值和最大值
for (int i = 0; i < array.length; i++) {
if (array[i] < min) {
min = array[i];
}
if (array[i] > max) {
max = array[i];
}
}
// 创建计数数组,用于统计每个元素出现的次数
int[] count = new int[max - min + 1];
// 统计数组中每个元素出现的次数
for (int i = 0; i < array.length; i++) {
count[array[i] - min]++;
}
// 将排序后的元素放回原数组
int arrayIndex = 0;
for (int i = 0; i < count.length; i++) {
// 对计数数组进行处理,将每个元素根据其计数放入原数组中
while (count[i] != 0) {
array[arrayIndex++] = i + min;
count[i]--;
}
}
}
解释:
int min = array[0]; int max = array[0];
:初始化最小值和最大值为数组的第一个元素。
for (int i = 0; i < array.length; i++)
:遍历数组,找到最小值和最大值。
int[] count = new int[max - min + 1];
:创建计数数组count
,其长度为max - min + 1
,用来存储每个值的出现次数。
for (int i = 0; i < array.length; i++)
:遍历数组并填充计数数组。
count[array[i] - min]++
:更新计数数组中的值,表示array[i]
出现的次数。
for (int i = 0; i < count.length; i++)
:遍历计数数组,并将排序后的元素放回原数组中。
while (count[i] != 0)
:根据计数数组的值将元素放入原数组中,每个元素按照计数的次数放入对应的位置。
——这样我们就实现了计数排序算法了!!!
(4)时间复杂度和空间复杂度
-
时间复杂度: O(n+k)O(n + k)O(n+k),其中 nnn 是数组的长度,kkk 是数组中元素的范围(最大值与最小值之间的差值)。
-
空间复杂度: O(k)O(k)O(k)。计数排序需要额外的计数数组,空间复杂度与元素的范围成正比。
(5)算法的应用场景
计数排序的特点使其适用于以下场景:
数据范围较小的整数排序: 计数排序适合整数范围较小的排序任务,如考试成绩、年龄等。
需要稳定排序的场景: 计数排序是一种稳定的排序算法,适用于需要保持相同元素相对顺序的场景。
数据分布相对均匀: 如果数据分布极度不均匀,计数排序的效率会大大降低,因此适用于数据分布较为均匀的情况。
——这样我们学习完了如何在Java中实现计数排序算法。
以上就是本篇文章的全部内容了~~~