概述

  • ​​冒泡排序​​
  • ​​应用1-把数组排成最小的数​​
  • ​​应用2-移动零到数组末尾​​

排序算法是一类非常经典的算法,说来简单,说难也难。刚学编程时大家都爱用冒泡排序,随后接触到选择排序、插入排序等,历史上还有昙花一现的希尔排序,公司面试时也经常会问到快速排序等等,小小的排序算法,融入了无数程序大牛的心血。

排序算法在生活中的应用非常广泛,比如:

  • 在学校时,每位学生的考试成绩会按照降序排出名次
  • 在电商领域,需要按照出单量排序,快速找出销量领先的商品
  • 在游戏清算时,根据用户的表现分评选出 MVP

在不同领域,排序算法的实现各有千秋。总体来看,排序算法大致可分为十类:

  • 选泡插:选择排序、冒泡排序、插入排序
  • 快归希堆:快速排序、归并排序、希尔排序、堆排序
  • 桶计基:桶排序、计数排序、基数排序

虽然工作中很少需要我们手打排序算法,只需要调用基础库中的 Arrays.sort() 便可解决排序问题。但你可曾静下心来,阅读 Arrays.sort() 背后的原理,它是采用了哪种排序算法呢?

事实上,Arrays.sort() 函数并没有采用单一的排序算法。Java 中的 Arrays.sort() 函数是由 Java 语言的几位创始人编写的,这个小小的函数逻辑严密,并且每个步骤都被精心设计,为了最大化性能做了一层又一层的优化,根据数据的概况采用双轴快排、归并或二分插入算法完成排序,堪称工业级排序算法的典范,理清之后其乐无穷。

并且,排序算法深受面试官的喜爱,在人才招聘时,总是将排序算法作为程序员的基本功来考察。对排序算法的理解深度在一定程度上反映了程序员逻辑思维的严谨度。攻克排序算法的难关是每位程序大牛的必经之路。

如牛顿所言,正是站在巨人的肩膀上,我们才能望得更远。本系列文章我们就来一起梳理一下排序算法的前世今生。

冒泡排序

冒泡排序是入门级的算法,但也有一些有趣的玩法。通常来说,冒泡排序有三种写法:

  • 一边比较一边向后两两交换,将最大值 / 最小值冒泡到最后一位;
  • 经过优化的写法:使用一个变量记录当前轮次的比较是否发生过交换,如果没有发生交换表示已经有序,不再继续排序;
  • 进一步优化的写法:除了使用变量记录当前轮次是否发生交换外,再使用一个变量记录上次发生交换的位置,下一轮排序时到达上次交换的位置就停止比较。
public static void main(String[] args) throws Exception {
int[] arr = new int[]{6, 2, 1, 3, 5, 4};
bubbleSort(arr);
// 输出: [1, 2, 3, 4, 5, 6]
System.out.println(Arrays.toString(arr));
}

public static void bubbleSort(int[] arr) {
boolean swapped = true;
// 最后一个没有经过排序的元素的下标
int indexOfLastUnsortedElement = arr.length - 1;
// 上次发生交换的位置
int swappedIndex = -1;
while (swapped) {
swapped = false;
for (int i = 0; i < indexOfLastUnsortedElement; i++) {
if (arr[i] > arr[i + 1]) {
// 如果左边的数大于右边的数,则交换,保证右边的数字最大
swap(arr, i, i + 1);
// 表示发生了交换
swapped = true;
// 更新交换的位置
swappedIndex = i;
}
}
// 最后一个没有经过排序的元素的下标就是最后一次发生交换的位置
indexOfLastUnsortedElement = swappedIndex;
}
}

// 交换元素
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

最外层的 while 循环每经过一轮,剩余数字中的最大值被移动到当前轮次的最后一位。

在下一轮比较时,只需比较到上一轮比较中,最后一次发生交换的位置即可。因为后面的所有元素都没有发生过交换,必然已经有序了。

当一轮比较中从头到尾都没有发生过交换,则表示整个列表已经有序,排序完成。

应用1-把数组排成最小的数

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

示例 1:

输入: [10,2]
输出: “102”

示例 2:

输入: [3,30,34,5,9]
输出: “3033459”

提示:

  • 0 < nums.length <= 100

说明:

  • 输出结果可能非常大,所以你需要返回一个字符串而不是整数
  • 拼接起来的数字可能会有前导 0,最后结果不需要去掉前导 0

解析:
这道题本质上是一道排序题,并且只能由基于比较的排序算法完成。如果 a 和 b 组成的字符串大于 b 和 a 组成的字符串,则交换 a 和 b。

代码:

public static void main(String[] args) {
int[] arr = new int[]{6, 2, 1, 3, 5, 4};
String minNumber = minNumber(arr);
// 输出: 123456
System.out.println(minNumber);
}

public static String minNumber(int[] nums) {
bubbleSort(nums);
return Arrays.toString(nums).replaceAll("\\[|]|,|\\s", "");
}

public static void bubbleSort(int[] arr) {
boolean swapped = true;
// 最后一个没有经过排序的元素的下标
int indexOfLastUnsortedElement = arr.length - 1;
// 上次发生交换的位置
int swappedIndex = -1;
while (swapped) {
swapped = false;
for (int i = 0; i < indexOfLastUnsortedElement; i++) {
if (("" + arr[i] + arr[i + 1]).compareTo("" + arr[i + 1] + arr[i]) > 0) {
// 如果 "" + arr[i] + arr[i + 1] 组成的字符串大于 "" + arr[i + 1] + arr[i] 组成的字符串,则交换
swap(arr, i, i + 1);
// 表示发生了交换
swapped = true;
// 更新交换的位置
swappedIndex = i;
}
}
// 最后一个没有经过排序的元素的下标就是最后一次发生交换的位置
indexOfLastUnsortedElement = swappedIndex;
}
}

// 交换元素
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

应用2-移动零到数组末尾

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

示例 2:

输入: nums = [0]
输出: [0]

提示:

  • 1 <= nums.length <= 104
  • -231 <= nums[i] <= 231 - 1

说明:

  • 输出结果可能非常大,所以你需要返回一个字符串而不是整数
  • 拼接起来的数字可能会有前导 0,最后结果不需要去掉前导 0

解析:
这道题数据量较小,可以使用冒泡排序的思想,将所有 0 依次交换到数组末尾。分析可知,冒泡排序不会改变本题中非零元素的相对顺序,所以符合题目中保持非零元素相对顺序的要求。

代码:

public static void main(String[] args) {
int[] arr = new int[]{6, 0, 1, 3, 0, 4};
moveZeroes(arr);
// 输出: [6, 1, 3, 4, 0, 0]
System.out.println(Arrays.toString(arr));
}

public static void moveZeroes(int[] nums) {
// 记录末尾 0 的数量
int zeroesCount = 0;
for (int i = 0; i < nums.length - zeroesCount; i++) {
if (nums[i] == 0) {
// 利用冒泡排序的思想,不断交换,将 0 移动到数组末尾
for (int j = i; j < nums.length - zeroesCount - 1; j++) {
exchange(nums, j, j + 1);
}
// 末尾多了一个 0,记录下来,以缩小遍历范围
zeroesCount++;
// 下一轮遍历时 i 会增加 1,但此时 nums[i] 已经和 nums[i+1] 交换了,nums[i+1] 还没有判断是否为 0,所以这里先减 1,以使下一轮继续判断 i 位置。
i--;
}
}
}

public static void exchange(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}