从荷兰国旗问题到快排优化升级

        如果在计算机领域存在一门技艺,放在十年、二十年后依然不过时,我想那必然是算法与数据结构。

一、荷兰国旗问题

                

        闲话少叙,直接进入正题,所谓荷兰国旗问题,就是给定一组数,请把小于某个数num的数放在左边,等于num的数放在中间,大于num的数放在右边。如果没有任何限制条件解决这个问题并不难,但要求在时间复杂度为O(n),额外空间复杂度为O(1)的条件下呢?

        好,3秒钟思考时间,3、2、1……对于没有长期坚持做算法题练习的人来,其实这个问题不算特别简单,下面直接给出解题答案:

        思路其实很简单,这里先给出以数组作为底层实现的解题方法,后文也会谈用链表该怎么做。我们先定义两个指针index1、index2,把数组划分为三块区域,下标小于等于index1左边部分数都小于num,介于index1和index2中间的数等于num,下标大于等于index2右边部分的数都大于num。然后从左至右遍历数组按照以下策略做相应的调整。

        1、遍历到的当前元素小于num,则把当前元素与index1的下一个元素做交换,同时index1++(即把遍历到的这个元素包含到小于区域中),遍历指针右移++。

        2、如果遍历到的元素等于num,则遍历指针右移++。

        3、如果遍历到的元素大于num,则把当前元素与index2的前一个元素做交换,同时index2--(把遍历到的这个元素包含到大于区域中),此时遍历指针不动,因为交换到当前位置的元素并未做比较,需要交由下一轮。

        按照以上思路写出代码如下:

public static void splitArrByNum(int[] arr, int num){
    if(arr == null || arr.length <= 1){
        return;
    }
    int index1 = -1;
    int index2 = arr.length;
    int i = 0;
    while(i < index2){
        if(arr[i] < num){
            swap(arr,i++,++index1);
        }else if(arr[i] == num){
            i++;
        }else{
            swap(arr,i,--index2);
        }
    }
}

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

       这里提到了一种花哨的交换元素的方法,原理在此不做赘述,但值得提醒的是这种交换元素的方法,要求待交换的元素所在内存地址不同,即:i != j,否则会有问题。      

二、快排优化升级

        熟悉快排的人应该很快能察觉到快排中就用用到了荷兰国旗问题的解题思路,就是通过一个基准数,不断划分待排序的区域,递归达到排序目的。当然不仅于此,下面聊聊快排的一个优化升级过程。

1.快排1.0

         1.0版本的快排也就是我们平时最常写的一种方式,代码如下:

    public void quickSort(int[] arr, int left, int right) {
        if (left < right) {
            int partitionIndex = partition(arr, left, right);
            quickSort(arr, left, partitionIndex - 1);
            quickSort(arr, partitionIndex + 1, right);
        }
    }

    private int partition(int[] arr, int left, int right) {
        // 设定基准值(pivot)
        int pivot = left;
        int index = pivot + 1;
        for (int i = index; i <= right; i++) {
            if (arr[i] < arr[pivot]) {
                swap(arr, i, index);
                index++;
            }
        }
        swap(arr, pivot, index - 1);
        return index - 1;
    }

        快排的时间复杂度为O(N*logN),上面的代码每次选取基准值的时候都选取的是最左边那个值,假如说原数组本身就有序,那么每次partitio获取到的下标都是最左边的那个值,使得快排变成了冒泡排序,时间复杂度达到了O(N^2),与我们原本的认知是相违背的。那么该怎么做进一步的优化呢?  

2.快排2.0

        为了避免每次都选取最左边的值作为基准值,那我们该选哪个值呢?最容易想到的就是随机找,好,按照这个思路我们的代码做下调整:

    public void quickSort(int[] arr, int left, int right) {
        if (left < right) {
            swap(arr, left, left+(int)(Math.random() *(right-left+1)));
            int partitionIndex = partition(arr, left, right);
            quickSort(arr, left, partitionIndex - 1);
            quickSort(arr, partitionIndex + 1, right);
        }
    }

        在做partition之前,把最左边的元素与数组中的任意随机位置做个交换,这样就使得每次选取的基准数保持随机,避免因数组本身有序而增大时间负责的问题。那这里大家想过没有,这里的时间复杂度是多少呢?或者说快排的时间复杂度究竟是怎么算出来的,这就牵扯到Master公式了。

3.Master公式 

        一般递归算法的时间复杂度计算会用到Master公式:

                Master公式:T(N) = a*T(N/b) + O(N^d)

        其中,a表示递归的次数,也就是生成的子问题数,b表示每次递归是原来数据量的1/b之一个规模,f(n)表示分解和合并所要花费的时间之和。

        当:

                logb(a) < d 时间复杂度为: O(N^d)

                logb(a) > d 时间复杂度为:O(N^logb(a))

                logb(a) == d 时间复杂度为: O(N^d * logN)

        分析快排算法,在最优的情况下,每次partition能把数据平均分为两个部分,此时的时间复杂度公式为:

        T(N) = 2T(N/2) + f(N)

        此时,a=2,b=2,d=1,因此:logb(a) == d, 从而得出时间复杂度为:O(N*logN)

        但是!以上说的是最优情况,也就是每次partition都能把数据平分成两部分才能达到这一时间复杂度,那如果采用随机选取基准值的方式做partition,时间复杂度会是多少呢?依然是O(N*logN)。为什么?这需要计算在随机情况下的数学期望,坦白来说,我不知道!在算法导论上有具体的证明,感兴趣的同学可以翻看《算法导论》。

4.快排3.0        

        前面谈到的排序算法,似乎还是跟荷兰国旗问题有点差异,下面利用荷兰国旗问题的解法再做进一步的优化。先分析下2.0版本还存在上面问题,我们通过基准数,把待排序的数组分成,小于基准数,大于基准数两个部分,以供下一轮partition排序,那对于等于基准数的那些元素呢?它们也被分到了这两部分中,其实按道理,等于基准数那些元素完全可以不参与到下一轮partition。想到这,我们直接就可以把荷兰国企问题的解法当做partition,最终形成快排3.0版本如下:   

public static void quickSort(int[] arr, int L, int R){
    if(L == R){
        return;
    }
    if(L < R){
        swap(arr,L+(int)(Math.random() *(R-L+1)), L);
        int[] p = partition(arr, L, R);
        quickSort(arr, L ,p[0]-1);
        quickSort(arr, p[1]+1, R);
    }
}
public static int[] partition(int[] arr,int L,int R){
    int less = L-1;
    int more = R;
    while(L < more){
        if(arr[L] < arr[R]){
            swap(arr, ++less, L++);
        } else if(arr[L] > arr[R]){
            swap(arr, --more, L);
        } else {
            L++;
        }
    }
    swap(arr, more, R);
    return new int[]{less+1,more};
}

         此时等于基准数的那些元素已经不参与到下一轮排序了,算法得到进一步优化。

        那么是否还可以做进一步的优化呢?从理论层面似乎较难,但从工程角度分析,我们可以尝试在算法调度方面采用快排思想,局部小范围排序采用插入排序,使得多种排序方式结合的方式,进一步优化算法。

public static void quickSort(int[] arr, int L, int R){
    if(L == R){
        return;
    }
    /**
     *  一些优化
     */
    if(L > R - 60){
        //采用插入排序
        //O(N^2)
        return;
    }
    if(L < R){
        swap(arr,L+(int)(Math.random() *(R-L+1)), L);
        int[] p = partition(arr, L, R);
        quickSort(arr, L ,p[0]-1);
        quickSort(arr, p[1]+1, R);
    }
}

三、荷兰国旗问题链表实现

        我们再回到荷兰国旗问题,以上是采用数组的形式存储数据,那如果是链表存储呢?

        用到6个变量:

                Node sH = null; //小于num部分的头指针

                Node sT = null; //小于num部分的尾指针

                Node eH = null; //等于num部分的头指针

                Node eT = null; //等于num部分的尾指针

                Node bH = null; //大于num部分的头指针

                Node bT = null; //大于num部分的尾指针

        通过这六个指针,把链表分成3各部分,再把这3部分指针首尾相连即可达到目的。但需要注意的是,可能不存在小于num的元素,或者不存在等于num的元素,或者大于num的元素,所以在做首尾相连的时候要做适当的判断。

public static Node listPartition(Node head, int pivot){
    Node sH = null;
    Node sT = null;
    Node eH = null;
    Node eT = null;
    Node bH = null;
    Node bT = null;
    Node next = null;
    while(head != null){
        next = head.next;
        head.next = null;
        if(head.value < pivot){
            if(sH ==null){
                sH = head;
                sT = head;
            } else {
                sT.next = head;
                sT = head;
            }
        }else if(head.value == pivot){
            if(eH ==null){
                eH = head;
                eT = head;
            } else {
                eT.next = head;
                eT = head;
            }
        }else{
            if(bH ==null){
                bH = head;
                bT = head;
            } else {
                bT.next = head;
                bT = head;
            }
        }
        head = next;
    }
    if(sT != null){
        sT.next = eH;
        eT = eT == null ? sT : eT;
    }
    if(eT != null){
        eT.next = bH;
    }
    return sH != null ? sH : (eH != null ? eH : bH);
}

            最后抛出一个问题,你知道JDK1.7之后,Arrays.sort排序做了什么优化吗?

作者:陈淅灿

猜你喜欢

转载自blog.csdn.net/vipshop_fin_dev/article/details/120407403