这篇文章讲一讲数基本的排序算法,看一下快慢指针在排序算法中的应用。
准确的说,这并不是快慢指针的应用,但使用快慢指针的角度可以更好地理解下面三种排序。
冒泡排序(Bubble Sort)
冒泡排序就是重复“从序列左边开始比较相邻两个数字的大小,再根据结果交换两个数字的位置”这一操作的算法。在这个过程中,数字会像泡泡一样,慢慢从左往右“浮”到序列的顶端,所以这个算法才被称为“冒泡排序”。
冒泡排序是暴力破解的一种,需要遍历所有元素,并与它相邻的元素比较。
假设数组 arr 有 n 个元素,则数组下标为 [0,n)
⨳ 第一次冒泡,遍历的元素 [0,n-1],遍历完成, [0,n-1] 中最大的元素冒泡到 索引 n-1 位置上,[n-1,n) 是排好序的
⨳ 第二次冒泡,遍历的元素 [0,n-2],遍历完成, [0,n-2] 中最大的元素冒泡到 索引 n-2 位置上, [n-2,n) 是排好序的
⨳ ....
⨳ 第k次冒泡,遍历的元素 [0,n-k],遍历完成,[0,n-k] 中最大的元素冒泡到 索引 n-k 位置上, [n-k,n) 是排好序的
⨳ ....
⨳ 第n次冒泡,遍历的元素 [0,0],遍历完成,[0,0] 中最大的元素冒泡到 索引0 位置上, [0,n) 是排好序的
也就是说,对于长度是 n 的数组需要进行 n 次冒泡(n次遍历),每次冒泡有要保证数组排好序的的部分 [n-k,n)增加一个元素(原地排序)。
冒泡排序如果使用快慢指针理解的话,慢指针用于记录每次冒泡出来的最大的元素,而快指针用于检索最大的元素,并将其交换到慢指针指定的位置。
void bubbleSort(int[] arr){
// 慢指针从尾到头遍历,记录当下最大的元素
for(int slow_index = arr.length-1; slow_index>=0; slow_index--){
// 慢指针从头到尾遍历,比较相邻元素,将较大的元素冒泡到 slow_index 指定位置
for(int fast_index=0;fast_index<slow_index;fast_index++){
if(arr[fast_index]>arr[fast_index+1]){
int tmp = arr[fast_index + 1];
arr[fast_index + 1] = arr[fast_index];
arr[fast_index] = tmp;
}
}
}
}
代码比较简单,但还可以优化,当某次冒泡遍历操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。
void bubbleSort2(int[] arr){
// 慢指针从尾到头遍历,记录当下最大的元素
for(int slow_index = arr.length-1; slow_index>=0; slow_index--){
boolean hasSwap = false; // 假设不需要交换,即当前数组已经排好序了
for(int fast_index=0;fast_index<slow_index;fast_index++){
if(arr[fast_index]>arr[fast_index+1]){
int tmp = arr[fast_index + 1];
arr[fast_index + 1] = arr[fast_index];
arr[fast_index] = tmp;
hasSwap = true;
}
}
if(!hasSwap) break; // 跳出循环
}
}
最好情况下,要排序的数据已经是有序的了,我们只需要进行一次冒泡操作,就可以结束了,所以最好情况时间复杂度是O(n)。而最坏的情况是,要排序的数据刚好是倒序排列的,我们需要进行n次冒泡操作,所以最坏情况时间复杂度为O(n^2)。
选择排序(Selection Sort)
选择排序就是重复“从待排序的数据中寻找最小值,将其与序列最左边的数字进行交换”这一操作的算法。
冒泡排序和选择排序最大的不同是:冒泡排序将较大/较小的元素交换到指定位置,而选择排序是将较大/较小的元素设置到指定位置。
void selectionSort(int[] arr){
// 慢指针从头到尾遍历,记录当下最小的元素
for(int slow_index=0;slow_index<arr.length;slow_index++){
// 选择 arr[slow_index,n) 中的最小值的索引
int min_index = slow_index; // 假设当前 slow_index 位置的元素就是最小值所在索引
for(int fast_index = slow_index+1;fast_index<arr.length;fast_index++){
if(arr[min_index]>arr[fast_index]){
min_index = fast_index;
}
}
// 将 arr[index,n) 中最小的元素插入到 slow_index 上
int tmp = arr[min_index];
arr[min_index] =arr[slow_index];
arr[slow_index] = tmp;
}
}
选择排序和冒泡排序相比,最大的优势就是只针对最小的元素进行交换,减少了交换次数,但这也不能说选择排序就比冒泡排序好,按时间复杂度上来说,就算要排序的数据已经是有序的了也需要 n 次选择最小值,也就是说选择排序的最好情况时间复杂度、最坏情况和平均情况时间复杂度都为O(n^2)。
插入排序(Insertion Sort)
选择排序和冒泡排序都是指定位置(慢指针指向的位置),寻找合适的元素(快指针检索元素),而插入排序则相反,是指定元素,寻找合适的位置。
怎么理解呢?
我们知道对于双指针来说,可以将数组动态看作是两部分,一部分是符合条件的,一部分是不符合条件的,比如对于选择排序来说,慢指针左部分都是排好序的,而慢指针右部分都是未排好序的。
对于插入排序也一样,如果将慢指针左部分看作是排好序的,如果想将慢指针指向的元素,插入到有序集合中,就要在有序集合中选择合适的位置进行插入,这是一个动态排序的过程。
插入排序和选择排序最大的不同就是:
⨳ 选择排序要插入的位置是确定的(在有序部分末尾),而插入的元素是不确定的,需要在无序部分查找最小值;
⨳ 插入排序要插入的位置是不确定的,而插入的元素是确定的(无序部分第一个元素)。
void insertionSort(int arr[]){
// arr[0,slow_index) 是有序的;arr[slow_index,n) 是无序的
for(int slow_index = 1;slow_index<arr.length;slow_index++){
// 在 arr[0,slow_index) 中寻找合适的位置
int insert_index = slow_index; // 假设要插入的位置,就是当前位置
int insert_value = arr[slow_index]; // 要插入的元素
// 寻找合适的位置
for(int fast_index=slow_index-1;fast_index>=0;fast_index--){
// 将要插入位置后的元素都后移一位
if(arr[fast_index]> insert_value){
arr[fast_index+1] = arr[fast_index];
insert_index = fast_index;
} else{
break;
}
}
// 插入元素
arr[insert_index] = insert_value;
}
}
如果要排序的数据已经是有序的,我们并不需要移动任何数据,每次循环只需要比较一个数据就能确定插入的位置。所以这种情况下,最好是时间复杂度为O(n)。
如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,所以最坏情况时间复杂度为O(n^2)。