快速排序、堆排序

快速排序

荷兰国旗问题(Dutch National Flag Problem)

给定一个数组arr,和一个数num;
请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。
要求额外空间复杂度O(1),时间复杂度O(N)

思路:
给定一个无序数组[4,5,6,7,2,1,9,8],num为5
使用一个变量p来划分小于等于num的范围,刚开始p=-1表示这个范围不存在。


image.png

遍历数组,当出现小于等于num的数后,当前遍历到的数字与p的下一个数字发生交换,p加1


image.png

本示例中遍历到数组的第一个数小于num,所以当前数字和p的下一个位置的数,即自己交换。后续过程省略,代码如下:
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

给定一个整数数组,给定一个值K,这个值在原数组中一定存在;
要求把数组中小于K的元素放到数组的左边,大于K的元素放到数组的右边,等于K的元素放到数组的中间;
最终返回一个整数数组,其中只有两个值,分别是等于K的数组部分的左右两个下标值。
例如:
给定数组:[2, 3, 1, 9, 7, 6, 1, 4, 5],给定一个值4,
那么经过处理原数组可能得一种情况是:[2, 3, 1, 1, 4, 9, 7, 6, 5].
需要注意的是,小于4的部分不需要有序,大于4的部分也不需要有序,返回等于4部分的左右两个下标,即[4, 4]

荷兰国旗问题较上问题增加了对等于区域划分的判断,在算法的思路上,我们可以再使用一个变量来跟踪大于num的区域:


image.png

num为5,当遍历到5时,p1与p2不发生变化,小于与大于区域不发生扩张;遍历到大于num的数时,当前遍历的数与p2前的数发生交换,p2减1,表示大于num的数扩张了一个单位,并且遍历的索引停止:


image.png

遍历到了数组的arr[2],发生交换后,继续判断arr[2]位置的数(8)与num的大小,直到当前遍历到的index与p2“相撞”后,停止。后续过程省略,代码如下:
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];
    }
}

经典快排与改进

经典快排

经典快排的思路即:partition+recursion
partition选择数组的最后一个数为参考值,将小于等于该数字的数罗列到左边,该数字放在中间,大于这个数的数罗列到该数字的右边,然后对该数字左边的数和右边的数进行递归,完成排序。经典快排以及测试代码如下:

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");
    }
}

改进快排

经典快排的问题是,每次只能排出一个数字,partition过程中是将num这个数字与小于等于区域和大于区域进行划分,如果有多个和num相等的数字,那么排序效率会变得十分低,由荷兰国旗问题的思考总结,我们可以进一步将快排进行优化改进。将partition过程变为将数组划分为小于区域,等于区域以及大于区域,然后对小于区域和大于区域再进行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];
    }
}

快排时间复杂度与额外空间复杂度分析

快排的时间复杂度与数据状况有关,一个有序的数组[0,1,2,3,4,5,6,7],如果使用快排排序,那么状况就是最差的。但是如果每次参考的基准数都能把数组均分成两部分那么我们就可以按照master公式来计算时间复杂度:T(N) = a*T(N/b) + O(N^d).快排分为两个子过程递归,除去递归外partition的时间复杂度为O(N),所以其时间复杂度为O(N^d * logN)即:O(NlogN)。但是如何使基准数能够将数组均分成两坨呢?为了解决这个问题,我们可以打乱当前的数据状况,sort部分的代码改动如下:

 public static void sort(int[] arr,int L,int R){
        if(L < R){
            swap(arr,L+(int)((R-L+1)*Math.random()),R);// 随机让任意位置的数与基准数位置R的数调换
            int [] indexArr = partition(arr,L,R);
            sort(arr,L,indexArr[0]-1);
            sort(arr,indexArr[1]+1,R);
        }
    }

通过添加一句代码,就可以让未知状况的数组取基准数,转换成一个概率问题,通过长期数学期望最终计算得到随即快速排序的时间复杂度为O(NlogN)。
随机快速排序的额外空间复杂度为O(logN),原因在于,partition最后要返回一个数组来记录与基准数相等的区域索引范围,这样的额外数组量为一个二叉树的高度即logN。

堆排序

最大堆与最小堆

满二叉树

什么是满二叉树?国内定义如果一个二叉树的层数为K,且节点的总数是2^K-1,那么这棵树就是一个满二叉树,示例:


image.png

完全二叉树

节点从左至右依次排布的树就是完全二叉树,示例:


image.png

堆是一种完全二叉树,分为最大堆(大根堆)和最小堆(小根堆)。当父节点的键值总是大于或等于任何一个子节点的键值时为最大堆,反之,当父节点的键值总是小于或等于任何一个子节点的键值时为最小堆。示例:


image.png

最大堆或最小堆的子树仍然是最大堆或最小堆。

heapInsert与heapify

heapInsert 与 heapify是堆结构中重要的两个方法。

heapInsert

heapInsert顾名思义,是向堆中插入节点,插入节点后,要判断该节点与其父节点的大小,如果大于父节点,那么则插入的节点与父节点交换位置,并继续向上判断,直到小于等于父节点为止。一个节点的父节点的索引为(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的作用举例:

假设有一个流,它源源不断向外吐出数字,这些数字是没有规律的;
现在希望你设计一种数据结构,可以快速地收集并获取吐出所有数字的中位数

如果你用一个数组去接收流吐出的数字,那么收集流吐出的数字很简单,但是如果要获取中位数的话就要对数组进行一次排序,如果要求流每次吐出一个数字就获取一次中位数,那么就要频繁地对数组进行排序操作。解决的方法就是使用堆结构来解决这个问题:设计一个最大堆,和一个最小堆,每次流吐出数字的时候都和最大堆的堆顶作比较,如果小于等于最大堆堆顶就放入最大堆,如果大于最大堆的堆顶则放入最小堆,入堆这个操作就是heapInsert。同时我们还需要保证最大堆的数据量不能比最小堆中的数据量大超过1,反之亦是如此/也就是要保持最大堆数据量N和最小堆的数据量M的关系为 |N-M| <= 1,如何保持这种关系呢?一旦破坏了这种关系的平衡,我们就让最大堆的顶部或者最小堆的顶部弹出,让这个最大的数字进入到最小堆里面,或者是让最小堆的最小的数字insert到最大堆里面,如图示意:


image.png

假设这个数据流吐出的数字依次是 5->6->4->7->3->8->1
左侧为最大堆数字数量为N,右侧为最小堆数字数量为M,没有破坏过 |N-M| <= 1的规定,如果这个时候数据流吐出的数字为0,那么就要heapInsert到最大堆里面,这个时候N-M = 2破坏了两堆数量上的平衡,所以要把最大堆的堆顶弹入到最小堆中去,操作如下:

首先将数字0 heapInsert到最大堆中


image.png

交换最大堆首尾数字


image.png

将最大堆末尾数字(5) heapInsert到最小堆
image.png

接下来就是heapify操作了,对最大堆中的堆顶0进行heapify,首先要判断它的两个孩子谁更大,谁大跟谁交换,直到满足最大堆的结构为止


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代表最大堆的数据,heapSize为最大堆数字的个数,index代表对哪个索引的数字进行heapify

堆排序

堆排序的思路就是对数组中的每个数字进行heapInsert操作,然后置换堆顶和堆末的数字,交换之后,最大的数字就排序好了,这时heapSize-1,然后对heapSize-1的这个堆的堆顶进行heapify,heapify操作后,这个heapSize-1的堆再次变为最大堆,然后再置换堆顶和堆末的数字,排出最大数字......代码如下:

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)所以堆排序的时间复杂度为O(NlogN),且堆排序的额外空间复杂度为O(1)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容