排序算法列舉以及優化

排序算法目前有很多算法,很多文章也做了一些講解,我也會爲了自己能有一份記錄和回溯,才記錄,直接開胃菜吧!

一、冒泡排序

針對這個算法大家已經耳熟能詳了,就是挨個元素和後面的元素進行對比,大的就和後面的元素進行位置替換,這樣每一輪都能保證當次循環的最大的元素會在右邊,知道循環結束,上算法代碼:

static void popPaiXu() {
    int[] nums = {1, 12, 8, 4, 10, 8, 9, 20, 39, 30};

    for (int i = nums.length - 1; i > 0; i--) {
        for (int j = 0; j < i; j++) {
            if (nums[j] > nums[j + 1]) {
                int temp = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = temp;
            }
        }
    }

    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + 、");
    }

}

打印如下:1、4、8、8、9、10、12、20、30、39、

首先我們現在的這個算法假如有10個元素,那麼就會for循環執行9次,雖然每次for循環的元素個數在減1,不過依舊可以看到有很多可以提升的空間。

場景1:比如說當我們在第三次就已經把序列排好了,那麼就不用繼續再排了,我們做一下改進:

static void popPaiXu() {
    int[] nums = {1, 12, 8, 4, 10, 8, 9, 20, 39, 30};
    for (int i = nums.length - 1; i > 0; i--) {
        System.out.println("lcs排序一次");
        //1
        boolean isNext = false; // 聲明一個變量來控制後續是否還需要進行遍歷冒泡
        for (int j = 0; j < i; j++) {
            if (nums[j] > nums[j + 1]) {
                int temp = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = temp;
                //2
                isNext = true;
            }
        }
        //3
        if (!isNext) {
            break;
        }
    }
    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + "、");
    }
}

代碼中我們加了一個變量isNext,每一次循環都初始化爲false,然後如果當前for循環沒有進行元素置換那說明現在的順序已經排好了,我們在2的地方把變量設置爲true,在3處直接break就好了,打印如下:

排序一次
排序一次
排序一次
1、4、8、8、9、10、12、20、30、39、

可以看到我的“排序一次”打印了三次,正常要打印9次,大大的提高了效率。

場景2:如果序列中最後的幾個元素的順序已經是排好的了,不一定是一開始就這樣,也許是在其中的某一次出現這種情況,那麼我們的優化點是忽略最後幾個連續的元素,我們只關注前面不連續的元素,也在一定程度上節約了性能,增加了排序的速度,代碼如下:

static void popPaiXu2() {
    int[] nums = {1, 12, 8, 4, 10, 8, 9, 20, 21, 22};

    for (int i = nums.length - 1; i > 0; i--) {
        System.out.println("lcs排序一次");
        //1
        int lastIndex = 0;  // 聲明一個index,記錄最後一次進行冒泡替換的索引值,下一次直接從這裏進行for循環,20\21\22本身就是有序的,所以無需進行for循環從而直接過
        for (int j = 0; j < i; j++) {
            if (nums[j] > nums[j + 1]) {
                int temp = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = temp;
                //2
                lastIndex = j + 1; // 把下一次要外部for循環的索引值存儲下來
            }
        }
        //3
        i = lastIndex;
    }

    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + "、");
    }

}

核心點是我們在1處聲明瞭一個索引值,初始化爲0,然後進入一樣的冒泡for循環,只要發生元素交換動作我們就把下一個元素的索引保存起來,這樣保證在最後一次交換元素操作完成之後我們保存的是交換元素下一個元素的索引值,重要的是3我們直接把索引值賦值給i,也就是外面的for循環,這樣for循環就從上一次的位置直接到了上一次最後交換元素的下一個元素的位置,因爲中間我們過濾到了一系列連續的元素,我們是不需要進行冒泡運算的了。

結果如下:

排序一次
排序一次
排序一次
1、4、8、8、9、10、12、20、21、22、

我們發現我們的for循環走了三次,同樣得到了優化,節約了性能。

二、選擇排序

選擇排序的算法也是非常簡單,和冒泡不一樣的是我們需要設定一個maxIndex索引,來保存我們最大值的索引,怎麼獲取最大值的索引呢,也是利用兩次for循環,依次拿第一個元素的值和後面的元素挨個作對比,發現有比它大的了,就把索引值當前比較大的元素的索引值賦值給maxIndex變量,這樣一輪一下我們就找到了當前for循環的最大的數值,然後進行元素的替換,把最大值的元素放到數組的最後,代碼如下:

static void selectPaiXu() {
    int[] nums = {1, 12, 8, 4, 10, 88, 9, 20, 21, 22};
    for (int i = nums.length - 1; i > 0; i--) {
        int maxIndex = 0;
        for (int j = 0; j < i; j++) {
            if (nums[maxIndex] <= nums[j + 1]) {
                maxIndex = j + 1;

            }
        }
        int temp = 0;
        temp = nums[maxIndex];
        nums[maxIndex] = nums[i];
        nums[i] = temp;
    }

    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + "、");
    }
}

結果如下:1、4、8、9、10、12、20、21、22、88、

三、插入排序

插入排序是一種非常有意思且比較高效的排序方法,同時插入排序是希爾排序的基礎,現在我們來看看插入排序

核心的思想就是在原數組的基礎之上創建了子數組的思想,剛開始的時候數組是無序的,其實還有一個有序的數組在前面,只是我們看不到,因爲它目前是空的而已。於是我們從無序的數組之中選中一個元素插入到這個有序的數組之中即可,起初在無序之中的第一個數組也可以稱之爲有序數組之中的起始數組的第一個元素,OK,因爲只有一個元素,所以也是有序的。於是我們依次從無序的數組之中拿取元素插入到有序的數組之中適當的位置即可,代碼如下:

static void chaRuPaiXu() {
    int[] nums = {1, 12, 8, 4, 10, 88, 9, 20, 21, 22};

    int cur;
    for (int i = 0; i < nums.length - 1; i++) {
        //1
        cur = nums[i + 1];
        //2
        int pre = i;
        while (pre >= 0 && cur < nums[pre]) {
            // 3
            nums[pre + 1] = nums[pre];
            pre--;
        }
        //4
        nums[pre + 1] = cur;
        
        for (int k = 0; k < nums.length; k++) {
            System.out.print(nums[k] + "、");
        }
        System.out.println(" ");
    }

    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + "、");
    }
}

首先我們獲取最靠近有序數組的元素也就是nums[i + 1]這個元素,保存起來,然後保存當前的索引值到pre之中,3然後拿當前的元素依次和有序數組之中的元素進行遍歷比較,當發現小於有序數組之中的元素的時候,就把大的元素賦值給後面的元素也就是我們當初選定的要插入到有序數組之中的那個值的位置,然後pre--,直到發現大於有序數組之中的值或者數組織中第一個值的時候,我們while也就結束了,然後最最重要的一步是我們要把有序數組之中當前小於我們起初的那個值也就是cur賦值成cur,因爲每一次命中while循環的時候都會出現兩個一樣的值,也就是nums[pre + 1] 和nums[pre]是相同的,當最後一次命中之後while循環就結束了,我們需要再一次把nums[pre + 1]進行最終的元素交換,也就是賦值我們起初的那個cur的值,這個cur也就是真正的找到了自己的位置了,我們也實現了一次插入數組的操作,結果如下:

1、12、8、4、10、88、9、20、21、22、 
1、8、12、4、10、88、9、20、21、22、 
1、4、8、12、10、88、9、20、21、22、 
1、4、8、10、12、88、9、20、21、22、 
1、4、8、10、12、88、9、20、21、22、 
1、4、8、9、10、12、88、20、21、22、 
1、4、8、9、10、12、20、88、21、22、 
1、4、8、9、10、12、20、21、88、22、 
1、4、8、9、10、12、20、21、22、88、 

上面這個打印是每一次的變化過程,while循環中的數據變化我列了一下前面幾個元素的變化規律,如下:

////        1、8、12、4;
////        1、8、12、12;
////        1、8、8、12;
////        1、4、8、12;
////        1、4、8、12、10;
////        1、4、8、12、12;
////        1、4、8、10、12;

每一次while命中之後都會出現兩個一樣的元素,這也是爲什麼要在最後我們要把cur值還原,不然就會丟失了,也就失去了插入排序的意義了。

最終結果如下:

1、4、8、9、10、12、20、21、22、88、

四、希爾排序

希爾排序的算法的基礎是插入排序,希爾排序又稱縮小增量排序,是希爾本人提出的,所以自己就命名爲希爾排序了。它通過比較相距一定間隔的元素來進行,各趟比較所用的距離隨着算法的進行而減小,直到只比較相鄰元素的最後一趟排序爲止。

希爾排序就是按照一定的gap值,不斷地對數組進行插入排序。不一樣的希爾排序算法可能採用不一樣的gap值。經典希爾算法的gap值爲N/2, N/4, ...... 直到gap值爲1,這裏的N爲數組的長度。

代碼如下:

static void xiErPaiXu() {
    int[] nums = {1, 12, 8, 4, 10, 88, 9, 20, 21, 22};
    int gap = nums.length;
    while (gap > 1) {
        //1
        gap = (int) Math.floor(gap / 2);
        System.out.println("gap - " + gap);
        int cur;
        for (int i = gap; i < nums.length; i++) {
            //2
            cur = nums[i];
            //3
            int pre = i - gap;
            while (pre > 0 && cur < nums[pre]) {
                //4
                nums[i] = nums[pre];
                pre -= gap;
            }
            //5
            nums[pre + gap] = cur;
        }
        for (int k = 0; k < nums.length; k++) {
            System.out.print(nums[k] + "、");
        }
        System.out.println(" ");
    }
    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + "、");
    }
}

我們在1處得到要分成的列數,總的元素是10個,所以第一次的列數是5,每個組是兩個元素進行比較,大的放到右邊,小的放到左邊,這一次下來基本上大的小的就一左一右分開了。在2處獲取當前元素的數值,這個跟插入排序是一樣的算法,在3處獲取上一個元素的索引,我們以gap爲長度依次遞減的,然後進行while循環,cur當前元素值依次和有序序列進行比較(插入排序有提到),從而在5的地方cur會插入到有序序列的適當的位置,然後gap爲1,也就是隻有一列了,此時的數組已經比較接近有序序列了,只要再進行一次插入排序,稍微的做一下調整即可達到有序序列了。

結果如下:

1, 12, 8, 4, 10, 88, 9, 20, 21, 22 //原數組

gap - 5
1、9、8、4、10、88、12、20、21、22、 第一次
gap - 2
1、4、8、9、10、20、12、22、21、88、 第二次
gap - 1
1、4、8、9、10、12、20、21、22、88、 第三次

爲什麼希爾排序要比插入排序高效呢?因爲當數組長度很大時,使用插入排序就會有個弊端,就是如果最小值排在很末端的時候,插入排序需要從末端開始,逐個往前比較到第一個位置,很低效。而希爾排序通過分組的方式,直接讓前端跟末端的元素進行比較,解決了插入排序的這個弊端。

當一開始 gap 很大的時候,每一個子數組的元素是很少的,所以對每個子數組用插入排序進行內部排序是很高效的;而後隨着gap不斷減小,這個數組會變的越來越有序了,此時使用插入排序也是很有利的了。

謝謝觀看!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章