排序算法
冒泡排序算法、插入排序算法、选择排序算法、希尔排序算法、归并排序算法、快速排序算法、计数排序算法、计数排序算法、桶排序算法
计数排序算法
通过统计序列中各个元素出现的次数,完成对整个序列的升序或降序排序,这样的排序算法称为计数排序算法。
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
- 计数排序的特征
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
通俗地理解,例如有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。
算法的步骤如下:
- (1)找出待排序的数组中最大和最小的元素
- (2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项
- (3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- (4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
代码演示
将4, 2, 2, 8, 3, 3, 1 用计数排序
public class Demo {
//找到数组中的最大值
public static int getMax(int[] list) {
int max = list[0];
for (int i = 1; i < list.length; i++) {
if (list[i] > max) {
max = list[i];
}
}
return max;
}
public static void countingSort(int[] list) {
int length = list.length;
//第 1 步,找到序列中的最大值
int max = getMax(list);
//第 2 步,初始化一个 array[max+1]
int[] array = new int[max + 1];
int[] output = new int[length];
//第 3 步,统计各个元素的出现次数,并存储在相应的位置上
for (int i = 0; i < length; i++) {
array[list[i]]++;
}
// 第 4 步,累加 array 数组中的出现次数
for (int i = 1; i <= max; i++) {
array[i] += array[i - 1];
}
// 第 5 步,根据 array 数组中的信息,找到各个元素排序后所在位置,存储在 output 数组中
for (int i = length - 1; i >= 0; i--) {
output[array[list[i]] - 1] = list[i];
array[list[i]]--;
}
// 将 output 数组中的数据原封不动地拷贝到 list 数组中
for (int i = 0; i < length; i++) {
list[i] = output[i];
}
}
public static void printList(int[] list) {
for (int i = 0; i < list.length; i++) {
System.out.print(list[i] + " ");
}
}
public static void main(String[] args) {
// 待排序序列
int[] list = new int[] { 4, 2, 2, 8, 3, 3, 1 };
//进行计数排序
countingSort(list);
printList(list);
}
}
希尔排序算法
希尔排序算法又叫缩小增量排序算法,是一种更高效的插入排序算法。和普通的插入排序算法相比,希尔排序算法减少了移动元素和比较元素大小的次数,从而提高了排序效率。
希尔排序算法的实现思路是:
- 将待排序序列划分成多个子序列,使用普通的插入排序算法对每个子序列进行排序;
- 按照不同的划分标准,重复执行第一步;
- 使用普通的插入排序算法对整个序列进行排序。
按照这个思路,我们尝试对 {35, 33, 42, 10, 14, 19, 27, 44} 做升序排序,具体的实现流程是:
- 间隔 4 个元素,将整个序列划分为 4 个子序列:
编辑
采用插入排序算法分别对 {35, 14}、{33, 19}、{42, 27}、{10, 44} 进行排序,最终生成的新序列为:
编辑
- 间隔 2 个元素,再次划分整个序列:
编辑
采用插入排序算法分别对 {14, 27, 35, 42} 和 {19, 10, 33, 44} 进行排序:
编辑
- 采用插入排序算法对整个序列进行一次排序,过程如下:
编辑
序列的划分方法
待排序序列如何进行划分,划分多少次,都会影响到希尔排序算法的执行效率。
希尔排序算法没有固定的划分标准,这里给大家推荐一种常用的方法,套用如下伪代码:
输入 list //输入待排序序列
interval <- 1 // 初始值为 1
while interval < length(list) / 3: // length(list) 表示待排序序列的长度
interval = interval * 3 + 1
经过计算得出的 interval 的值,就是首次划分序列采用的标准。
后续划分整个序列,套用如下公式:
interval = (interval-1)/3
比如说计算第二次划分序列的标准,只需将第一次划分序列时计算得到的 interval 代入公式,求出的新 interval 值就是第二次采用的划分标准。
代码实现
public class Demo01 {
// list[N] 为存储待排序序列的数组
public static void shell_sort(int[] list) {
int length = list.length;
// 初始化间隔数为 1
int interval = 1;
// 计算最大间隔
while (interval < length / 3) {
interval = interval * 3 + 1;
}
// 根据间隔数,不断划分序列,并对各子序列排序
while (interval > 0) {
// 对各个子序列做直接插入排序
for (int i = interval; i < length; i++) {
int temp = list[i];
int j = i;
while (j > interval - 1 && list[j - interval] >= temp) {
list[j] = list[j - interval];
j -= interval;
}
if (j != i) {
list[j] = temp;
}
}
// 计算新的间隔数,继续划分序列
interval = (interval - 1) / 3;
}
}
public static void main(String[] args) {
int[] list = { 35, 33, 42, 10, 14, 19, 27, 44 };
shell_sort(list);
// 输出已排好序的序列
for (int i = 0; i < list.length; i++) {
System.out.print(list[i] + " ");
}
}
}
归并排序算法
归并排序算法是在分治算法基础上设计出来的一种排序算法,它可以对指定序列完成升序(由小到大)或降序(由大到小)排序,对应的时间复杂度为O(nlogn)。
归并排序算法实现排序的思路是:
- 将整个待排序序列划分成多个不可再分的子序列,每个子序列中仅有 1 个元素;
- 所有的子序列进行两两合并,合并过程中完成排序操作,最终合并得到的新序列就是有序序列。
举个简单的例子,使用归并排序算法对 {7, 5, 2, 4, 1, 6, 3, 0} 实现升序排序的过程是:
- 将 {7, 5, 2, 4, 1, 6, 3, 0} 分割成多个子序列,每个子序列中仅包含 1 个元素,分割过程如下所示:
编辑
图 1 归并排序算法分割序列的过程
整个序列不断地被一分为二,最终被分割成 {7}、{5}、{2}、{4}、{1}、{6}、{3}、{0} 这几个序列。
- 将 {7}、{5}、{2}、{4}、{1}、{6}、{3}、{0} 以“两两合并”的方式重新整合为一个有序序列,合并的过程如下图所示:
编辑
图 2 归并排序算法整合所有子序列的过程
归并排序算法的具体实现
对比图 1 和图 2 很容易联想到,归并排序算法可以借助递归的思想实现,对应的伪代码如下:
输入 arr[n] // 输入要排序的序列
merge_sort(arr[n] , p , q): // [p , q] 表示对第 p ~ q 区域内的元素进行归并排序
if p < q : // 对 [p , q] 区域不断采用对半分割的方式,最终将整个区域划分为多个仅包含 1 个元素(p==q)的序列
mid = ⌊(p+q)/2⌋
merge_sort(arr , p , mid)
merge_sort(arr , mid+1 , q)
merge(arr , p , mid , q) // 调用实现归并过程的代码模块
merge_sort() 用于将整个序列分割成多个子序列,merge() 用来合并这些子序列,合并的实现方式为:
- 从 [p, mid] 和 [mid+1, q] 两个区域的元素分别拷贝到 leftarr 和 rightarr 区域。
- 从 leftarr 和 rightarr 区域中各个取出第一个元素,比较它们的大小;
- 将较小的元素拷贝到 [p, q] 区域,然后从较小元素所在的区域内取出下一个元素,继续进行比较;
- 重复执行第 3 步,直至 leftarr 和 rightarr 内的元素全部拷贝到 [p, q] 为止。如果 leftarr 或者 rightarr 有一方为空,则直接将另一方的所有元素依次拷贝到 [p, q] 区域。
代码实现
public class Demo02{
//实现归并排序算法的分割操作
public static void merge_sort(int[] arr, int p, int q) {
// 如果数组不存在或者 [p.q] 区域不合理
if (arr == null || p >= q) {
return;
}
//对[p,q]区域进行分割
int mid = (p + q) / 2;
merge_sort(arr, p, mid);
merge_sort(arr, mid + 1, q);
//对分割的 [p,mid] 和 [mid+1,q] 区域进行归并
merge(arr, p, mid, q);
}
//实现归并排序算法的归并操作
public static void merge(int[] arr, int p, int mid, int q) {
int numL = mid - p + 1;
int numR = q - mid;
//创建 2 个数组,分别存储 [p,mid] 和 [mid+1,q]区域内的元素
int[] leftarr = new int[numL + 1];
int[] rightarr = new int[numR + 1];
int i;
for (i = 0; i < numL; i++) {
leftarr[i] = arr[p - 1 + i];
}
//将 leftarr 数组中最后一个元素设置为足够大的数。
leftarr[i] = 2147483647;
for (i = 0; i < numR; i++) {
rightarr[i] = arr[mid + i];
}
//将 rightarr 数组中最后一个元素设置为足够大的数。
rightarr[i] = 2147483647;
int j = 0;
i = 0;
//对 leftarr 和 rightarr 数组中存储的 2 个区域的元素做归并操作
for (int k = p; k <= q; k++) {
if (leftarr[i] <= rightarr[j]) {
arr[k - 1] = leftarr[i];
i++;
} else {
arr[k - 1] = rightarr[j];
j++;
}
}
}
public static void main(String[] args) {
int[] arr = new int[] { 7, 5, 2, 4, 1, 6, 3, 0 };
//对 arr 数组中第 1 至 8 个元素进行归并排序
merge_sort(arr, 1, 8);
for (int i : arr) {
System.out.print(i + " ");
}
}
}
桶排序算法
桶排序(又称箱排序)是一种基于分治思想、效率很高的排序算法,理想情况下对应的时间复杂度为 O(n)。
接下来,我们系统地学习一下桶排序算法。
桶排序算法的实现思路
假设一种场景,对 {5, 2, 1, 4, 3} 进行升序排序,桶排序算法的实现思路是:
- 准备 5 个桶,从 1~5 对它们进行编号;
- 将待排序序列的各个元素放置到相同编号的桶中;
- 从 1 号桶开始,依次获取桶中放置的元素,得到的就是一个升序序列。
整个实现思路如下图所示:
编辑
桶排序算法中,待排序的数据量和桶的数量并不一定是简单的“一对一”的关系,更多场景中是“多对一”的关系,例如,使用桶排序算法对 {11, 9, 21, 8, 17, 19, 13, 1, 24, 12} 进行升序排序,实现过程如下图所示:
编辑
待排序序列中有 10 个元素,但算法中只用了 5 个桶,因此有些桶需要存放多个元素。实际场景中,我们可以自定义各个桶存放元素的区间(范围),比如上图中第一个桶存放 [0,5) 区间内的元素,第二个桶存放 [6,10) 之间的元素。
当存在“一个桶中有多个元素”的情况时,要先使用合适的排序算法对各个痛内的元素进行排序,然后再根据桶的次序逐一取出所有元素,最终得到的才是一个有序序列。
总之,桶排序算法的实现思路是:将待排序序列中的元素根据规则分组,每一组采用快排、插入排序等算法进行排序,然后再按照次序将所有元素合并,就可以得到一个有序序列。
桶排序算法的具体实现
假设用桶排序算法对 {0.42, 0.32, 0.23, 0.52, 0.25, 0.47, 0.51} 进行升序排序,采用的分组规则是:将所有元素分为 10 组,每组的标号分别为 0~9。对序列中的各个元素乘以 10 再取整,得到的值即为该元素所在组的组号。
编辑
代码实现
import java.util.ArrayList;
import java.util.Collections;
public class BucketSort {
public static void bucketSort(float[] arr) {
int n = arr.length;
if (n <= 0)
return;
@SuppressWarnings("unchecked")
ArrayList<Float>[] bucket = new ArrayList[n];
// 创建空桶
for (int i = 0; i < n; i++)
bucket[i] = new ArrayList<Float>();
// 根据规则将序列中元素分散到桶中
for (int i = 0; i < n; i++) {
int bucketIndex = (int) arr[i] * n;
bucket[bucketIndex].add(arr[i]);
}
// 对各个桶内的元素进行排序
for (int i = 0; i < n; i++) {
Collections.sort((bucket[i]));
}
// 合并所有桶内的元素
int index = 0;
for (int i = 0; i < n; i++) {
for (int j = 0, size = bucket[i].size(); j < size; j++) {
arr[index++] = bucket[i].get(j);
}
}
}
public static void main(String[] args) {
float[] arr = { (float) 0.42, (float) 0.32, (float) 0.23, (float) 0.52, (float) 0.25, (float) 0.47,
(float) 0.51 };
bucketSort(arr);
for (float i : arr)
System.out.print(i + " ");
}
}