快速排序、堆排序

快速排序

荷蘭國旗問題(Dutch National Flag Problem)

給定一個數(shù)組arr油狂,和一個數(shù)num;
請把小于等于num的數(shù)放在數(shù)組的左邊,大于num的數(shù)放在數(shù)組的右邊。
要求額外空間復(fù)雜度O(1)粪糙,時間復(fù)雜度O(N)

思路:
給定一個無序數(shù)組[4,5,6,7,2,1,9,8],num為5
使用一個變量p來劃分小于等于num的范圍忿项,剛開始p=-1表示這個范圍不存在猜旬。


image.png

遍歷數(shù)組,當出現(xiàn)小于等于num的數(shù)后倦卖,當前遍歷到的數(shù)字與p的下一個數(shù)字發(fā)生交換,p加1


image.png

本示例中遍歷到數(shù)組的第一個數(shù)小于num椿争,所以當前數(shù)字和p的下一個位置的數(shù),即自己交換怕膛。后續(xù)過程省略,代碼如下:
public class Patition {
    public static void patition(int []arr,int num){
        if(arr == null)
            return;

        int p = -1;
        for(int i = 0;i<arr.length;i++){
            if(arr[i]<=num)
                swap(arr,i,++p);
        }
        return ;
    }

    public static void swap(int []arr,int i,int j){
        if(arr == null || i == j)
            return;
        arr[i] = arr[i]^arr[j];
        arr[j] = arr[i]^arr[j];
        arr[i] = arr[i]^arr[j];
    }
}

問題的升級:(荷蘭國旗問題)Dutch National Flag Problem

給定一個整數(shù)數(shù)組秦踪,給定一個值K褐捻,這個值在原數(shù)組中一定存在;
要求把數(shù)組中小于K的元素放到數(shù)組的左邊掸茅,大于K的元素放到數(shù)組的右邊,等于K的元素放到數(shù)組的中間;
最終返回一個整數(shù)數(shù)組柠逞,其中只有兩個值昧狮,分別是等于K的數(shù)組部分的左右兩個下標值。
例如:
給定數(shù)組:[2, 3, 1, 9, 7, 6, 1, 4, 5]板壮,給定一個值4,
那么經(jīng)過處理原數(shù)組可能得一種情況是:[2, 3, 1, 1, 4, 9, 7, 6, 5].
需要注意的是逗鸣,小于4的部分不需要有序,大于4的部分也不需要有序绰精,返回等于4部分的左右兩個下標撒璧,即[4, 4]

荷蘭國旗問題較上問題增加了對等于區(qū)域劃分的判斷,在算法的思路上笨使,我們可以再使用一個變量來跟蹤大于num的區(qū)域:


image.png

num為5卿樱,當遍歷到5時,p1與p2不發(fā)生變化硫椰,小于與大于區(qū)域不發(fā)生擴張繁调;遍歷到大于num的數(shù)時,當前遍歷的數(shù)與p2前的數(shù)發(fā)生交換靶草,p2減1蹄胰,表示大于num的數(shù)擴張了一個單位,并且遍歷的索引停止:


image.png

遍歷到了數(shù)組的arr[2]爱致,發(fā)生交換后烤送,繼續(xù)判斷arr[2]位置的數(shù)(8)與num的大小,直到當前遍歷到的index與p2“相撞”后,停止糠悯。后續(xù)過程省略,代碼如下:
public class DutchNationalFlag {

    public static int[] partition(int []arr,int L,int R,int num){
        if(arr == null)
            return null;

        int p1 = L - 1;
        int p2 = R + 1;
        int curIndex = L;
        while(curIndex < p2){
            if(arr[curIndex] < num){
               swap(arr,curIndex++,++p1);
            }else if(arr[curIndex] == num){
                curIndex++;
            }else{
                swap(arr,curIndex,--p2);
            }
        }
        return new int[]{++p1,--p2};
    }

    public static void swap(int[] arr,int i,int j){
        if(arr == null || i == j)
            return;

        arr[i] = arr[i]^arr[j];
        arr[j] = arr[i]^arr[j];
        arr[i] = arr[i]^arr[j];
    }
}

經(jīng)典快排與改進

經(jīng)典快排

經(jīng)典快排的思路即:partition+recursion
partition選擇數(shù)組的最后一個數(shù)為參考值帮坚,將小于等于該數(shù)字的數(shù)羅列到左邊,該數(shù)字放在中間互艾,大于這個數(shù)的數(shù)羅列到該數(shù)字的右邊试和,然后對該數(shù)字左邊的數(shù)和右邊的數(shù)進行遞歸,完成排序纫普。經(jīng)典快排以及測試代碼如下:

import java.util.Arrays;

public class ClassicQuickSort {

    public static void quickSort(int [] arr){
        sort(arr,0,arr.length-1);
    }

    public static void sort(int []arr,int L,int R){
        if(L < R){
            int mid = partition(arr,L,R);
            sort(arr,L,mid-1);
            sort(arr,mid+1,R);
        }
    }
    public static int partition(int []arr,int L,int R){
        int p = L-1;
        for(int i = L;i<R+1;i++){
            if(arr[i]<=arr[R])
                swap(arr,i,++p);
        }
        return p;
    }

    public static void swap(int []arr,int i,int j){
        if(arr == null || i == j)
            return;

        arr[i] = arr[i]^arr[j];
        arr[j] = arr[i]^arr[j];
        arr[i] = arr[i]^arr[j];
    }

    // test:comparator
    public static void comparator(int []arr){
        Arrays.sort(arr);
    }

    // test :printer
    public static void printArray(int[]arr){
        if(arr == null)
            return;

        for(int i = 0;i<arr.length;i++){
            System.out.print(arr[i]+ " ");
        }
        System.out.println();
    }
    // test: copier
    public static int[] copier(int []arr){
        if(arr == null)
            return null;

        int [] copyArr = new int[arr.length];
        for(int i = 0;i<arr.length;i++){
            copyArr[i] = arr[i];
        }

        return copyArr;
    }
    // test: generateRandomArray
    public static int[] generateRandomArray(int maxSize,int maxValue){
        int size = (int)((maxSize+1)*Math.random());
        int[] arr = new int[size];
        for(int i = 0;i<size;i++){
            // (-maxValue,maxValue)
            arr[i] = (int)((maxValue+1)*(Math.random()) - maxValue*Math.random());
        }
        return arr;
    }
    // test: isEqual
    public static boolean isEqual(int[] arr1, int[] arr2){
        if(arr1 != null&& arr2 == null || arr2 != null && arr1 == null)
            return false;
        if(arr1.length != arr2.length)
            return false;
        if(arr1 == null && arr2 == null)
            return true;

        for(int i = 0;i<arr1.length;i++){
            if(arr1[i] != arr2[i])
                return false;
        }

        return true;
    }

  public static void main(String[]args){
        int testTime = 5000;
        int maxSize = 100;
        int maxValue = 100;
        boolean succeed = true;
        for(int i = 0;i<testTime;i++){
            int [] arr1 = generateRandomArray(maxSize,maxValue);
            int [] arr2 = copier(arr1);
            quickSort(arr1);
            comparator(arr2);
            if(!isEqual(arr1,arr2)){
                succeed = false;
                printArray(arr1);
                printArray(arr2);
                break;
            }
        }
        System.out.println(succeed?"Nice":"Wrong");
    }
}

改進快排

經(jīng)典快排的問題是阅悍,每次只能排出一個數(shù)字,partition過程中是將num這個數(shù)字與小于等于區(qū)域和大于區(qū)域進行劃分昨稼,如果有多個和num相等的數(shù)字节视,那么排序效率會變得十分低,由荷蘭國旗問題的思考總結(jié)假栓,我們可以進一步將快排進行優(yōu)化改進寻行。將partition過程變?yōu)閷?shù)組劃分為小于區(qū)域,等于區(qū)域以及大于區(qū)域匾荆,然后對小于區(qū)域和大于區(qū)域再進行partition的遞歸拌蜘。代碼如下杆烁,省略測試代碼:

public class QuickSort {

    public static void quickSort(int[] arr){
        sort(arr,0,arr.length-1);
    }

    public static void sort(int[] arr,int L,int R){
        if(L < R){
            int [] indexArr = partition(arr,L,R);
            sort(arr,L,indexArr[0]-1);
            sort(arr,indexArr[1]+1,R);
        }
    }
    public static int[] partition(int[] arr,int L,int R){
        int p1 = L - 1;
        int p2 = R ;
        int curIndex = L;
        while(curIndex < p2){
            if(arr[curIndex] < arr[R]){
                swap(arr,curIndex++,++p1);
            }else if(arr[curIndex] > arr[R]){
                swap(arr,curIndex,--p2);
            }else{
                curIndex++;
            }
        }
        swap(arr,p2,R);
        return new int[]{++p1,--p2};
    }

    public static void swap(int[] arr,int i,int j){
        if(arr == null || i == j)
            return;

        arr[i] = arr[i]^arr[j];
        arr[j] = arr[i]^arr[j];
        arr[i] = arr[i]^arr[j];
    }
}

快排時間復(fù)雜度與額外空間復(fù)雜度分析

快排的時間復(fù)雜度與數(shù)據(jù)狀況有關(guān),一個有序的數(shù)組[0,1,2,3,4,5,6,7],如果使用快排排序简卧,那么狀況就是最差的兔魂。但是如果每次參考的基準數(shù)都能把數(shù)組均分成兩部分那么我們就可以按照master公式來計算時間復(fù)雜度:T(N) = a*T(N/b) + O(N^d).快排分為兩個子過程遞歸,除去遞歸外partition的時間復(fù)雜度為O(N),所以其時間復(fù)雜度為O(N^d * logN)即:O(NlogN)举娩。但是如何使基準數(shù)能夠?qū)?shù)組均分成兩坨呢析校?為了解決這個問題,我們可以打亂當前的數(shù)據(jù)狀況晓铆,sort部分的代碼改動如下:

 public static void sort(int[] arr,int L,int R){
        if(L < R){
            swap(arr,L+(int)((R-L+1)*Math.random()),R);// 隨機讓任意位置的數(shù)與基準數(shù)位置R的數(shù)調(diào)換
            int [] indexArr = partition(arr,L,R);
            sort(arr,L,indexArr[0]-1);
            sort(arr,indexArr[1]+1,R);
        }
    }

通過添加一句代碼勺良,就可以讓未知狀況的數(shù)組取基準數(shù),轉(zhuǎn)換成一個概率問題骄噪,通過長期數(shù)學期望最終計算得到隨即快速排序的時間復(fù)雜度為O(NlogN)尚困。
隨機快速排序的額外空間復(fù)雜度為O(logN),原因在于,partition最后要返回一個數(shù)組來記錄與基準數(shù)相等的區(qū)域索引范圍链蕊,這樣的額外數(shù)組量為一個二叉樹的高度即logN事甜。

堆排序

最大堆與最小堆

滿二叉樹

什么是滿二叉樹?國內(nèi)定義如果一個二叉樹的層數(shù)為K,且節(jié)點的總數(shù)是2^K-1,那么這棵樹就是一個滿二叉樹滔韵,示例:


image.png

完全二叉樹

節(jié)點從左至右依次排布的樹就是完全二叉樹逻谦,示例:


image.png

堆是一種完全二叉樹,分為最大堆(大根堆)和最小堆(小根堆)陪蜻。當父節(jié)點的鍵值總是大于或等于任何一個子節(jié)點的鍵值時為最大堆邦马,反之,當父節(jié)點的鍵值總是小于或等于任何一個子節(jié)點的鍵值時為最小堆宴卖。示例:


image.png

最大堆或最小堆的子樹仍然是最大堆或最小堆滋将。

heapInsert與heapify

heapInsert 與 heapify是堆結(jié)構(gòu)中重要的兩個方法。

heapInsert

heapInsert顧名思義症昏,是向堆中插入節(jié)點随闽,插入節(jié)點后,要判斷該節(jié)點與其父節(jié)點的大小肝谭,如果大于父節(jié)點掘宪,那么則插入的節(jié)點與父節(jié)點交換位置,并繼續(xù)向上判斷攘烛,直到小于等于父節(jié)點為止魏滚。一個節(jié)點的父節(jié)點的索引為(index-1)/2。

public static void heapInsert(int[] arr,int index){
        while(arr[index] > arr[(index-1)/2]){
            swap(arr,index,(index-1)/2);
            index = (index-1)/2;
        }
    }
public static void swap(int[] arr,int i,int j){
        if(arr == null || i == j)
            return;

        arr[i] = arr[i]^arr[j];
        arr[j] = arr[i]^arr[j];
        arr[i] = arr[i]^arr[j];
    }

heapify

heapify的作用舉例:

假設(shè)有一個流坟漱,它源源不斷向外吐出數(shù)字栏赴,這些數(shù)字是沒有規(guī)律的;
現(xiàn)在希望你設(shè)計一種數(shù)據(jù)結(jié)構(gòu),可以快速地收集并獲取吐出所有數(shù)字的中位數(shù)

如果你用一個數(shù)組去接收流吐出的數(shù)字须眷,那么收集流吐出的數(shù)字很簡單,但是如果要獲取中位數(shù)的話就要對數(shù)組進行一次排序沟突,如果要求流每次吐出一個數(shù)字就獲取一次中位數(shù)花颗,那么就要頻繁地對數(shù)組進行排序操作。解決的方法就是使用堆結(jié)構(gòu)來解決這個問題:設(shè)計一個最大堆惠拭,和一個最小堆扩劝,每次流吐出數(shù)字的時候都和最大堆的堆頂作比較,如果小于等于最大堆堆頂就放入最大堆职辅,如果大于最大堆的堆頂則放入最小堆棒呛,入堆這個操作就是heapInsert。同時我們還需要保證最大堆的數(shù)據(jù)量不能比最小堆中的數(shù)據(jù)量大超過1域携,反之亦是如此/也就是要保持最大堆數(shù)據(jù)量N和最小堆的數(shù)據(jù)量M的關(guān)系為 |N-M| <= 1,如何保持這種關(guān)系呢簇秒?一旦破壞了這種關(guān)系的平衡,我們就讓最大堆的頂部或者最小堆的頂部彈出秀鞭,讓這個最大的數(shù)字進入到最小堆里面趋观,或者是讓最小堆的最小的數(shù)字insert到最大堆里面,如圖示意:


image.png

假設(shè)這個數(shù)據(jù)流吐出的數(shù)字依次是 5->6->4->7->3->8->1
左側(cè)為最大堆數(shù)字數(shù)量為N锋边,右側(cè)為最小堆數(shù)字數(shù)量為M,沒有破壞過 |N-M| <= 1的規(guī)定皱坛,如果這個時候數(shù)據(jù)流吐出的數(shù)字為0,那么就要heapInsert到最大堆里面豆巨,這個時候N-M = 2破壞了兩堆數(shù)量上的平衡剩辟,所以要把最大堆的堆頂彈入到最小堆中去,操作如下:

首先將數(shù)字0 heapInsert到最大堆中


image.png

交換最大堆首尾數(shù)字


image.png

將最大堆末尾數(shù)字(5) heapInsert到最小堆
image.png

接下來就是heapify操作了往扔,對最大堆中的堆頂0進行heapify,首先要判斷它的兩個孩子誰更大贩猎,誰大跟誰交換,直到滿足最大堆的結(jié)構(gòu)為止


image.png

heapify的代碼如下:
public static void heapify(int[] arr,int heapSize,int index){
        int leftIndex = 2*index+1;
        while(leftIndex < heapSize){
            int largestIndex = leftIndex+1<heapSize && arr[leftIndex]<arr[leftIndex+1]?leftIndex+1:leftIndex;
            largestIndex = arr[index]>arr[largestIndex]?index:largestIndex;
            if(largestIndex != index){
                swap(arr,index,largestIndex);
                index = largestIndex;
                leftIndex = largestIndex*2+1;
            }else {
                break;
            }
        }
    }

arr代表最大堆的數(shù)據(jù)瓤球,heapSize為最大堆數(shù)字的個數(shù)融欧,index代表對哪個索引的數(shù)字進行heapify

堆排序

堆排序的思路就是對數(shù)組中的每個數(shù)字進行heapInsert操作,然后置換堆頂和堆末的數(shù)字卦羡,交換之后噪馏,最大的數(shù)字就排序好了,這時heapSize-1绿饵,然后對heapSize-1的這個堆的堆頂進行heapify,heapify操作后欠肾,這個heapSize-1的堆再次變?yōu)樽畲蠖眩缓笤僦脫Q堆頂和堆末的數(shù)字拟赊,排出最大數(shù)字......代碼如下:

public class HeapSort {

    public static void heapSort(int[] arr){
        // 形成最大堆
        for(int i = 0;i<arr.length;i++){
            heapInsert(arr,i);
        }

        int heapSize = arr.length;
        
        while(heapSize > 0){
            swap(arr,0,heapSize-1);
            heapSize--;
            heapify(arr,heapSize,0);
        }

    }

    public static void swap(int[] arr,int i,int j){
        if(arr == null || i == j)
            return;

        arr[i] = arr[i]^arr[j];
        arr[j] = arr[i]^arr[j];
        arr[i] = arr[i]^arr[j];
    }

    public static void heapInsert(int[] arr,int index){
        while(arr[index] > arr[(index-1)/2]){
            swap(arr,index,(index-1)/2);
            index = (index-1)/2;
        }
    }

    public static void heapify(int[] arr,int heapSize,int index){
        int leftIndex = 2*index+1;
        while(leftIndex < heapSize){
            int largestIndex = leftIndex+1<heapSize && arr[leftIndex]<arr[leftIndex+1]?leftIndex+1:leftIndex;
            largestIndex = arr[index]>arr[largestIndex]?index:largestIndex;
            if(largestIndex != index){
                swap(arr,index,largestIndex);
                index = largestIndex;
                leftIndex = largestIndex*2+1;
            }else {
                break;
            }
        }
    }
}

堆排序heapInsert花費的時間為log1+log2+log3+log4...+logN刺桃;heapify的操作也是相當于每個元素依次遍歷二叉樹的高度,因為有O(log1+log2+...+logn)=O(log(n!))=O(nlogn)所以堆排序的時間復(fù)雜度為O(NlogN)吸祟,且堆排序的額外空間復(fù)雜度為O(1)瑟慈。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市葛碧,隨后出現(xiàn)的幾起案子蔗衡,更是在濱河造成了極大的恐慌洋措,老刑警劉巖堆生,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蔗怠,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門氛谜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澳腹,“玉大人,你說我怎么就攤上這事“莆茫” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么定踱? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任至扰,我火速辦了婚禮,結(jié)果婚禮上圾结,老公的妹妹穿的比我還像新娘筝野。我一直安慰自己晌姚,他們只是感情好,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布歇竟。 她就那樣靜靜地躺著挥唠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪焕议。 梳的紋絲不亂的頭發(fā)上宝磨,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機與錄音盅安,去河邊找鬼唤锉。 笑死,一個胖子當著我的面吹牛别瞭,可吹牛的內(nèi)容都是我干的窿祥。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼畜隶,長吁一口氣:“原來是場噩夢啊……” “哼壁肋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起籽慢,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤浸遗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后箱亿,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體跛锌,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年届惋,在試婚紗的時候發(fā)現(xiàn)自己被綠了髓帽。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡脑豹,死狀恐怖郑藏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瘩欺,我是刑警寧澤必盖,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布拌牲,位于F島的核電站,受9級特大地震影響歌粥,放射性物質(zhì)發(fā)生泄漏塌忽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一失驶、第九天 我趴在偏房一處隱蔽的房頂上張望土居。 院中可真熱鬧,春花似錦嬉探、人聲如沸擦耀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽埂奈。三九已至,卻和暖如春定躏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背芹敌。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工痊远, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人氏捞。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓碧聪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親液茎。 傳聞我的和親對象是個殘疾皇子逞姿,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容