排序算法几乎是每个程序开发者都要必备的技能,从大学期间C语言学习的冒泡和选择排序,到后来数据结构课程学习的各种其他排序。这篇文章再次梳理一下这些经典的排序算法。
各种排序算法的时间复杂度
如上图,这里列出了各种排序的时间和空间复杂度。记得在校招期间,一个面试官问我们一群学生,排序算法中那个最快。很多的同学回答了快速排序,然后被直接的pass了,当时我回答了一个堆排序,拿到了后面继续面试的资格。
可以看到快速排序和堆排序这里平均的情况是一样的都是o(nlog2 n),但是在当序列已经接近排好的情况时快速排序的时间复杂度是o(n^2),堆排序没有变。并且堆排序的空间复杂度比快速排序要小。
这里的稳定指的是相同元素,会不会占用内存。
什么时候用什么排序呢?
- 如果元素比较多,并且比较乱,选择快速排序。
- 如果元素比较多,并且提前有序,选择堆排序。
- 如果元素相同的元素比较多,这里选择归并排序。
交换排序
交换排序是每次比较,如果元素和序列不同就要交换。故为交换排序。
冒泡排序
算法思想:比较相邻的元素。如果第一个比第二个大,就交换他们两个。
private static void sortByBubble(int[] num) {
//第一层循环从数组的最后往前遍历
for (int i = num.length - 1; i > 0; --i) {
//这里循环的上界是 i - 1,在这里体现出 “将每一趟排序选出来的最大的数从sorted中移除”
for (int j = 0; j < i; j++) {
//保证在相邻的两个数中比较选出最大的并且进行交换(冒泡过程)
if (num[j] > num[j + 1]) {
int temp = num[j];
num[j] = num[j + 1];
num[j + 1] = temp;
}
}
for (int k = 0; k < num.length; k++) {
System.out.print(num[k] + " ");
}
System.out.print("\n");
}
}
记忆技巧:冒泡是将最大元素冒出来,第一个最大冒出来之后,下一次再遍历的时候比较的元素会少一,这里双层for循环。
- 第一层循环从数组的最后往前遍历,这里循环的上界是 i - 1,在这里体现出 “将每一趟排序选出来的最大的数从sorted中移除。
- 第二层循环是从0到本次循环的最大值,也就是i,是一个递增的过程,如果不合适,就交换。
快速排序
算法思想:本质来说,快速排序是冒泡排序的升级版,也是一种交换排序
由于快速排序是一种分治算法,我们可以用分治思想将快排分为三个步骤:
- 分:设定一个分割值,并根据它将数据分为两部分
- 治:分别在两部分用递归的方式,继续使用快速排序法
- 合:对分割的部分排序直到完成
代码实现
private static int getMiddle(int[] list, int low, int high) {
int tmp = list[low];//数组的第一个作为中轴
while (low < high) {
while (low < high && list[high] >= tmp) {
high--;
}
list[low] = list[high]; //比中轴小的记录移到低端
while (low < high && list[low] <= tmp) {
low++;
}
list[high] = list[low]; //比中轴大的记录移到高端
}
list[low] = tmp; //中轴记录到尾
return low; //返回中轴的位置
}
private static void quickSort(int[] list, int low, int high){
if (low < high) {
int middle = getMiddle(list, low, high); //将list数组进行一分为二
quickSort(list, low, middle - 1); //对低字表进行递归排序
quickSort(list, middle + 1, high); //对高字表进行递归排序
}
}
记忆技巧:
- 取得中轴元素,完成交换的三步,两步在大while内,两小while后,结合图来写。
- 递归拿到进行排序,知道low<high 不成立
- 测试调用quickSort(num,0,num.length-1);
选择排序
选择排序是在一轮比较之后,选择最值,然后和排序开始位置交换。
直接选择排序
基本思想:在要排序的一组数中,选出最小的一个数与第一个位置的数交换;然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。
private static void selectSort(int[] nums) {
int k, temp; //定义k为最小值的索引,temp是用于交换的临时变量
for (int i = 0; i < nums.length; i++) {
k = i; //每次假设第一个最小
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] < nums[k]) {
k = j;//k是动态记录最小值,这里只记录最小的下标。
}
}
temp = nums[i];
nums[i] = nums[k];
nums[k] = temp;
}
}
记忆技巧: 双层for循环,第二层只记录最值位置,在第一层内部进行交换。
堆排序
什么是堆?
对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序基本思想及步骤
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
-
步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
-
1.假设给定无序序列结构如下,先标号,然后构造一个完全二叉树
-
2.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。
-
3.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
-
这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
-
1.假设给定无序序列结构如下,先标号,然后构造一个完全二叉树
-
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
-
a.将堆顶元素9和末尾元素4进行交换
-
b.重新调整结构,使其继续满足堆定义
-
c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.
-
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
代码实现:
-
a.将堆顶元素9和末尾元素4进行交换
private static void heapSort(int[] arr) {
//1.构建大顶堆
for (int i = arr.length / 2; i >= 0; i--) {
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr, i, arr.length);
}
for (int j = arr.length - 1; j > 0; j--) {
swap(arr, 0, j);
adjustHeap(arr, 0, j);
}
}
private static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i];//先取出当前元素i
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {//从i结点的左子结点开始,也就是2i+1处开始
if (k + 1 < length && arr[k] < arr[k + 1]) {//如果左子结点小于右子结点,k指向右子结点
k++;
}
if (arr[k] > temp) {//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
arr[i] = arr[k];
i = k;
} else {
break;
}
}
arr[i] = temp;//将temp值放到最终的位置
}
插入排序
插入排序像我们打扑克,把刚拿到的牌放到合适的位置。
直接插入排序
思想:每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的字序列的合适位置(从后向前找到合适位置后),直到全部插入排序完为止。
代码实现:
/**
* 跳格子比较
* @param nums
*/
public static void insertSort(int nums[]){
//插入排序是把原有序列的元素插入到已经排好的序列中。需要双层for循环。
//第一层第一次for循环从第二个元素开始往第一个元素插入。一直到最后一个num
//第二层要比较从0开始到i-1的所有元素
for (int i = 1 ;i< nums.length;i++){
for (int j = 0;j < i;j++ ){
if (nums[i]<nums[j]){
//如果不满足条件,进行交换
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
}
}
记忆技巧:
- 插入排序是把原有序列的元素插入到已经排好的序列中。需要双层for循环。
- 第一层第一次for循环从第二个元素开始往第一个元素插入。一直到最后一个num
- 第二层要比较从0开始到i-1的所有元素
希尔排序
基本思想:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。
-
图解
代码实现
private static void shellSort(int[] nums) {
for (int increment = nums.length / 2; increment > 0; increment /= 2) {
System.out.println("increment:" + increment);
for (int i = increment; i < nums.length; i++) {
System.out.println(" i:" + i);
for (int j = i; j >= increment; j -= increment) {
System.out.print(" j:" + j + ",j-increment:" + (j - increment));
if (nums[i] < nums[j - increment]) {
int temp = nums[i];
nums[i] = nums[j - increment];
nums[j - increment] = temp;
} else {
break;
}
}
System.out.println();
}
}
}
以上就是我对算法排序的理解。