数组 之 简单排序

67 阅读6分钟

数组部分,已经讲了 数组简介快慢指针前后指针

这篇文章讲一讲数基本的排序算法,看一下快慢指针在排序算法中的应用。

准确的说,这并不是快慢指针的应用,但使用快慢指针的角度可以更好地理解下面三种排序。

冒泡排序(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)。