冒泡排序
冒泡排序的核心就是依次比较相邻的两个数,升序排序时将小数放在前面,大数放在后面。排序算法一般都需要进行多轮比较,以下是冒泡排序的升序比较过程。
第 1 轮:首先比较第 1 个和第 2 个数,将小数放前,大数放后;然后比较第 2 个数和第 3 个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后;至此第一轮结束,将最大的数放到了最后。
第 2 轮:仍从第一对数开始比较,将小数放前,大数放后,一直比较到倒数第二个数(倒数第一的位置上已经是最大的数),第二轮结束,在倒数第二的位置上得到一个新的最大数(其实在整个数列中是第二大的数)。
总结来说,第 1 到 n-1 轮中(n 为数组长度,下文同)第 i 轮的作用是把第 i 大的数放到数组的 n-i 下标处。按此规律操作,直至最终完成排序。由于在排序过程中总是将大数往后放,类似于气泡往上升,所以称作冒泡排序。
通过上面的分析可以看出,假设需要排序的序列的个数是 n,则需要经过 n-1 轮,最终完成排序。在第一轮中,比较的次数是 n-1 次,之后每轮减少 1 次。
冒泡排序示例动图(来源:https://www.cnblogs.com/onepixel/p/7674659.html)如下:
插入排序
本节介绍的最基础的插入排序算法。它将待排序的数据分为两个部分,第一部分中的数据是已经排好序的(初始时,第一个数据划入第一部分),第二部分中的数据是无序的。之后,每次从第二部分取出头部(即第一个)元素,把它插入到第一部分的合适位置,使插入后的第一部分仍然有序。直接插入排序的具体流程如下:
第一轮:以下标 1 的元素(记为 a1,后面类似)为头部向前找插入位置,如果 a1 ≥a0 ,则维持现状;否则 a1 向前插(a0 后移,位置 0 处将 a1 放入)。
第二轮:以 a2 为头部向前找插入位置,如果 a2 ≥ a1 ,则维持现状;否则 a1 向后挪。继续与 a0 比较,此时如果 a2 ≥a0 ,则在位置 1 处放入 a2 ,如果 a0 > a2 ,将 a0 后挪一位,在位置 0 处将 a2 放入。
总的来说,第 i 轮时,ai 为待定位的数据,通过与前序数据比较并将更大的数据后挪(如有必要)的方式找到合适的位置插入 ai,使得数组的前 i+1 个元素有序。一共要进行 n-1 轮。
代码:
插入排序示例动图(来源:https://www.cnblogs.com/onepixel/p/7674659.html)如下:
快速排序
快速排序是对冒泡排序的一种改进,是通过每一趟排序,将要排序的数组(或后续讲解的集合)分割成两个独立的部分。其中,一部分的所有数据比另一部分的所有数据都要小。然后通过递归重复这种操作,对分割后的两部分数据分别进行快速排序,最终达到整个数据都是有序排列的。下面的章节将会介绍单向扫描法和双向扫描法两种方式的快速排序。
存在两种方式:单项扫描,双向扫描
快速排序之单向扫描
- 选定数组的一个元素,将之称为“主元”。之后,扫描一趟数组,将大于或等于主元的元素放在主元的右边,把小于或等于主元的元素放在主元的左边,这个过程被称为用主元分割数组。具体做法是:
a. 选定数组的第一个元素(即 nums 数组中的元素 4)作为主元。
b. 定义两个标记变量 sp 和 bigger,它们都是数组下标。其中 sp 表示 1 中,在从左往右扫描一趟数组的过程中,当前正在扫描的位置,它会向右移动;bigger 是边界,其右侧的数据大于或等于主元。初始时,sp 指向数组的第二个元素,bigger 指向数组的最后一个元素,如图所示。
c. 假设数组名是 arr,第一趟的比较流程是:在 sp≤bigger 的情况下循环,比较 arr[sp]<=主元 是否成立,如果是,sp 右移一位;否则,就交换 arr[sp] 和 arr[bigger],并将 bigger 的位置左移一位(注意 sp 原地不动),第一次比较过程如图所示。
数据交换前:
数据交换后:
bigger 左移一位:
d. 继续循环,重复 c 中描述的过程,如下。
接"bigger 左移一位:"如图,因为当前的arr[sp]<=主元(1<4)成立,所以 sp 右移一位即 sp++;
此时 arr[sp]<=主元(6>4)不成立,所以交换 arr[sp] 和 arr[bigger],再将 bigger 左移一位,如下图所示。
数据交换前:
数据交换后:
bigger 左移一位:
因为arr[sp]<=主元(2 < 4)成立,sp 右移一位,如图所示。
sp 右移一位:
因为 arr[sp]<=主元(7>4)不成立,所以交换 arr[sp] 和 arr[bigger],再将 bigger 左移一位,如下图所示。
数据交换前:
数据交换后:
bigger 左移一位:
此时,sp==bigger,仍要继续判断,此处因为 arr[sp]<=主元(3<4)成立,所以 sp 还会右移一次变为大于 bigger,而 bigger 保持不动。
至此,循环结束,bigger 右侧的数据全部大于或等于主元,注意 bigger 本身指向的数据是小于主元的,下面通过交换 arr[bigger] 和主元就可以完成以主元分割数组的任务。
e. 交换 arr[bigger] 和主元,如图所示。
- 此时,主元左边的元素都小于主元,主元右边的元素都大于或等于主元。即主元已经是处在排好序的位置了。
- 将此时的数组以主元为界,分割为两部分,分别对两部分递归处理,即从a. 开始——选取新的主元(图中的新主元 1 和新主元 2),如图所示。
用同样的方法,将新的主元也放置到排好序的位置。
- 递归的结束条件是:子数组只有 1 个元素或者 0 个元素。
快速排序之单向扫描代码
新建一个 TestQuickSort.java 文件,并提供类的代码:
public class TestQuickSort {
//核心代码及main方法
}
排序算法中会用到很多的数据交换,所以我们先实现方法 swap(int[] arr, int index1, int index2) 实现对数组 arr 的 index1 和 index2 下标位置的元素实现调换:
//交换arr[index1]和arr[index2]
static void swap(int[] arr, int index1, int index2) {
int tmp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = tmp;
}
设计方法 pv(int[] arr, int l, int r) 方法实现一次单向扫描,返回边界的下标:
//单向扫描的排序
static int pv(int[] arr, int l, int r) {
// 主元
int p = arr[l];
// 扫描指针
int sp = l + 1;
int bigger = r;
while (sp <= bigger) {
if (arr[sp] <= p)
sp++;
else {
swap(arr,sp,bigger);
bigger--;
}
}
swap(arr,l,bigger);
return bigger;
}
完成快速排序算法 quickSort(int[] arr, int l, int r):
//递归调用单向扫描方法
static void quickSort(int[] arr, int l, int r) {
if (l < r) {
int q = pv(arr, l, r);
quickSort(arr, l, q - 1);
quickSort(arr, q + 1, r);
}
}
组合代码并进行测试
public class TestQuickSort {
public static void main(String[] args) {
int[] a = {3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};
System.out.println("排序前的数据为:");
for (int a1 : a) {
System.out.print(a1+" ");
}
quickSort(a, 0, 14);
System.out.println("\n"+"排序后的数据为:");
for (int a2 : a) {
System.out.print(a2+" ");
}
}
//单向扫描的排序
static int pv(int[] arr, int l, int r) {
// 主元
int p = arr[l];
// 扫描指针
int sp = l + 1;
int bigger = r;
while (sp <= bigger) {
if (arr[sp] <= p)
sp++;
else {
swap(arr,sp,bigger);
bigger--;
}
}
swap(arr,l,bigger);
return bigger;
}
//递归调用单向扫描方法
static void quickSort(int[] arr, int l, int r) {
if (l < r) {
int q = pv(arr, l, r);
quickSort(arr, l, q - 1);
quickSort(arr, q + 1, r);
}
}
//交换arr[index1]和arr[index2]
static void swap(int[] arr, int index1, int index2) {
int tmp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = tmp;
}
}
编译、运行此程序,结果如下图所示。
至此我们已经完成单向扫描法实现快速排序。快速排序在理解起来比较复杂,需要仔细分析和研究。
双向扫描法
双向扫描法仍然是选取第一个元素为主元,然后在主元以外的元素里,从左右两侧同时扫描,如下图中的 left、right。
left 在向右扫描(移动)的过程中,如果 arr[left]<=主元 成立,则 left 右移,否则停止移动;right 向左扫描的过程中,如果 arr[right]>=主元 成立,则 right 左移,否则停止移动。当 left 和 right 都停止移动时,如果这时 left<=right,则交换左右 arr[left] 和 arr[right],如下图所示。
扫描停止并准备交换数据:
交换后的数据:
之后继续向中间遍历,直到 left> right,如下。
a. 上一个图中,arr[left]<=主元 成立,所以 left 右移;arr[right]>=主元 成立,所以 right 左移,如图所示。
此时,left<right,因此需要交换 arr[left]和 arr[right],如图所示。 right 左移数据交换前:
right 左移数据交换后:
b. 上一个图中,arr[left]<=主元成立,所以 left 右移;arr[right]>=主元成立,所以 right 左移,如图所示。
此时,left<=right 成立,且 left 和 right 满足停止条件,因此需要交换 arr[left] 和 arr[right],如下图所示。 数据交换前:
数据交换后:
c. 上一个图中,arr[left]<=主元 成立,所以 left 右移;arr[right]>=主元 成立,所以 right 左移,如图所示。
此时,已达到了循环退出条件 left> right,因此循环终止。
d. 再交换主元与 arry[right],如下图所示。
主元与 arry[right]交换前:
主元与 arry[right]交换后:
至此,主元也处在了合适的位置上,一趟排序结束。
再用递归,将主元左侧和右侧的子数组视为两个需要排序的数组,重复以上步骤即可实现对整个数组的排序。
双向扫描代码实现
双向扫描法原理与单向扫描法思想上相同,只是使用不同的划分策略,同单向扫描不同点就在于扫描方法及调用的参数上,接下来我们详细看一下。
新建一个 TestDoubleQuickSort.java 文件,补充类的代码:
public class TestDoubleQuickSort {
//核心代码及main方法
}
我们直接展示双向扫描的核心代码:
设计方法 pv2(int[] arr, int l, int r) 方法实现一次双扫描,返回边界的下标:
static int pv2(int[] arr, int l, int r) {
int p = arr[l];
int left = l + 1;
int right = r;
while (left <= right) {
// left向右走,直到遇见大于主元的元素
while (left <= right && arr[left] <= p)
left++;
// right向左走,直到遇见小于或等于主元的元素
while (left <= right && arr[right] > p)
right–;
if (left < right) {
swap(arr, left, right);
}
}
// while退出时,left, right两者交错,且right指向最后一个小于等于主元的元素,
// 也就是主元应该在的位置
swap(arr, l, right);
return right;
}
补充完成方法调用等内容,完整代码如下:
public class TestDoubleQuickSort {
public static void main(String[] args) {
int[] a = {12,32,13,45,34,65,76,78,89,57};
System.out.println("排序前的数据为:");
for (int a1 : a) {
System.out.print(a1+" ");
}
quickSort(a, 0, 9);
System.out.println("\n"+"排序后的数据为:");
for (int a2 : a) {
System.out.print(a2+" ");
}
}
static int pv2(int[] arr, int l, int r) {
int p = arr[l];
int left = l + 1;
int right = r;
while (left <= right) {
// left向右走,直到遇见大于主元的元素
while (left <= right && arr[left] <= p)
left++;
// right向左走,直到遇见小于或等于主元的元素
while (left <= right && arr[right] > p)
right--;
if (left < right) {
swap(arr, left, right);
}
}
// while退出时,left, right两者交错,且right指向最后一个小于等于主元的元素,
// 也就是主元应该在的位置
swap(arr, l, right);
return right;
}
//递归调用双指针扫描方法
static void quickSort(int[] arr, int l, int r) {
if (l < r) {
int q = pv2(arr, l, r);
quickSort(arr, l, q - 1);
quickSort(arr, q + 1, r);
}
}
//交换arr[index1]和arr[index2]
static void swap(int[] arr, int index1, int index2) {
int tmp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = tmp;
}
}
编译、运行此程序,结果如下图所示。
快速排序已成为工业界的排序标准,因其易于实现且性能稳定。这里我们介绍排序算法的目的主要是加深对数组、方法和递归的理解,不准备深入探讨算法。