快速排序計算第K大的數

        先說結論,最終版本代碼如下:

public class KthNum {
    public static int k = 2;
    public static boolean bigK = false;

    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int kNum = quickSort(arr);
        System.out.println("kNum=" + kNum);
    }

    public static int quickSort(int arr[]) {
        int length = arr.length;
        if (k <= 0 || k > length) throw new RuntimeException("K值不合理");
        int left = 0, right = length - 1;
        int p = -1;
        while (k != p + 1) {
            if (k < p + 1) {
                right = p - 1;
            } else if (k > p + 1) {
                left = p + 1;
            }
            p = partition(arr, left, right);
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (bigK ? arr[arrIndex] > pivot : arr[arrIndex] < pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

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


}

      還是老規矩,我會重點談思路,到底是如何想出這段代碼的,請往下看。

       熟悉快排的同學都知道,若不熟悉的同學,可以先看我的這篇白話解析快速排序。快排使用某一個數作爲基準點進行分區,通常基準點左邊的數都是小於基準點的,在基準點右邊的數都是大於基準點的。

        例如1,3,4,2這組數字,使用快排以2爲基準點進行倒序排序第一次的結果爲3,4,2,1 ,如果你恰好是求第3大的數,那麼就是基準點2,只用了一次排序,時間複雜度爲O(n)。如果你是求第1大的數或者第2大的數,那麼繼續在基準點左邊進行排序比較即可;如果你是求第4大的數,那麼在基準點右邊進行排序比較即可。細心的同學可以發現求第K大的數,K的值是和基準點的下標有關係的。第一次排序完成後,基準點2的下標爲2,使用下標+1K進行比較,若K等於下標+1直接返回當前下標的值;若K大於下標+1,則繼續在基準點右邊進行查找;若K小於下標+1,則繼續在基準點左邊進行查找。循環查找,直到K等於下標+1,則結束。

       基於以上的思路和對快排的理解,我們得出了計算第K大數的第一個版本1.0。

 1.0

public class KthNum {
    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int k = 2;
//        k=4;
        int kNum = quickSort(arr, 0, arr.length - 1, k);
        System.out.println("kNum=" + kNum);

    }

    public static int quickSort(int arr[], int left, int right, int k) {
        if (left >= right) return -1;
        int p = partition(arr, left, right);
        while (k != p + 1) {
            if (k < p + 1) {
                p = partition(arr, left, p - 1);
            } else if (k > p + 1) {
                p = partition(arr, p + 1, right);
            }
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (arr[arrIndex] > pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

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

以上代碼在K爲2時,可以正常求出第2大的數爲6,。但是當K爲4時,會發生死循環。

那麼爲什麼發生死循環:

思路1:

仔細查看while循環中 if 和 else if 中的代碼,分析其執行過程如下:

上圖是每一次執行完 partition 方法後,基準點的下標 p 的位置。通過分析第9次和第14次的執行結果,可以發現如果繼續往下走就會進入9-13次的死循環。現在已經發現了問題,那麼造成問題的原因是什麼?

2個問題導致的:

     1. while循環中left 和  right 的值一直沒發生變化

     2. 通過分析第8次的執行結果,可以發現2個相同的值5在比較時不會發生數據交換,這導致7, 6, 5, 4, 5, 3, 3, 2, 1 中的第4個數和第5個數無法形成有序的位置。

思路2:

      將while循環中的代碼和快排中的代碼對比會發現,在循環中 left 和  right 的值一直沒發生變化的。初步估計會導致已經排過序的數下次循環會繼續參與排序,從而導致死循環。

1.0小結:

主要是想通過複製粘貼的方式快速實現第K大數的方案,偷懶的心態導致了此版本中的死循環問題

 1.1

··修復1.0版本中的死循環問題:

public class KthNum {
    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int k = 2;
        k=4;
        int kNum = quickSort(arr, 0, arr.length - 1, k);
        System.out.println("kNum=" + kNum);

    }

    public static int quickSort(int arr[], int left, int right, int k) {
        if (left >= right) return -1;
        int p = partition(arr, left, right);
        while (k != p + 1) {
            if (k < p + 1) {
                p = partition(arr, left, p - 1);
            } else if (k > p + 1) {
                p = partition(arr, p + 1, right);
            }
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (arr[arrIndex] >= pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

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


}

修改了 partition 方法中第4行的代碼,從 arr[arrIndex] > pivot 修改爲 arr[arrIndex] >= pivot 。也就是在2個數相等時依然可以進行比較數據交換。

 2.0

··修復1.0版本中的while循環中left 和  right 的值一直沒發生變化導致死循環問題,也是我們的主線版本:

public class KthNum {
    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int k = 2;
        k=4;
        int kNum = quickSort(arr, 0, arr.length - 1, k);
        System.out.println("kNum=" + kNum);

    }

    public static int quickSort(int arr[], int left, int right, int k) {
        if (left >= right) return -1;
        int p = partition(arr, left, right);
        while (k != p + 1) {
            if(k<p+1){
                right=p-1;
            }else if(k>p+1){
                left=p+1;
            }
            p=partition(arr,left,right);
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (arr[arrIndex] > pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

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


}

修改了 quickSort 方法中第5行和第7行的代碼,將while循環中的 partition 方法調用放到了 if 外面,在 if 和 else if 中修改 left 和right 的值,這樣使用快排計算第K大的數就基本實現了。

3.0:

這個版本是對2.0版本的優化:

public class KthNum {
    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int k = 2;
        k=4;
        int kNum = quickSort(arr, 0, arr.length - 1, k);
        System.out.println("kNum=" + kNum);
    }

    public static int quickSort(int arr[], int left, int right, int k) {
        if (left >= right) return -1;
        int p=-1;
        while (k != p + 1) {
            if(k<p+1){
                right=p-1;
            }else if(k>p+1){
                left=p+1;
            }
            p=partition(arr,left,right);
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (arr[arrIndex] > pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

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


}

修改了 quickSort 方法中第2行的代碼,這裏的思路是 quickSort  方法中的第2行和第9行都調用了 partition 方法,我希望可以把它們統一起來,於是我給p賦值爲-1,統一調用while循環中的partition方法。

4.0:

這個版本是對3.0版本的調整優化:

public class KthNum {
    public static int k = 4;

    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int kNum = quickSort(arr);
        System.out.println("kNum=" + kNum);
    }

    public static int quickSort(int arr[]) {
        int length = arr.length;
        if (k <= 0 || k > length) throw new RuntimeException("K值不合理");
        int left = 0, right = length - 1;
        int p = -1;
        while (k != p + 1) {
            if (k < p + 1) {
                right = p - 1;
            } else if (k > p + 1) {
                left = p + 1;
            }
            p = partition(arr, left, right);
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (arr[arrIndex] > pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

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


}

優化點:

    1. 將 quickSort 方法的局部參數 left 和 right 放到方法內部賦值

    2. 移除了原quickSort 方法中 left 和 right  的比較

    3. quickSort 方法中添加了 K 值合理性的判斷

4.0小結髮散思考:

       至此,快排計算第K大的數就完成了。我們還是發散思考一下,既然第K大的數,那麼肯定也有求第K小的數的需求。帶着這個問題,我們重新審視一下上述4.0的代碼,可以得出2種方案:

       1. 既然求第K大的數在while循環中可以通過K和P+1,在不同的區間範圍排序查找。那麼同理,求第K小的數也可以通過K和P的關係算出來。

       2. 上述 partition 方法中是通過倒序的方式求第K大的數,那麼求第K小的數只需要採用升序的方式即可。

4.1:

這個版本就是4.0小結中的求第K小的數的第1種解決方案:

public class KthNum {
    public static int k = 3;
    public static boolean bigK = false;

    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int kNum = quickSort(arr);
        System.out.println("kNum=" + kNum);
    }

    public static int quickSort(int arr[]) {
        int length = arr.length;
        if (k <= 0 || k > length) throw new RuntimeException("K值不合理");
        int left = 0, right = length - 1;
        int p = bigK ? -1 : partition(arr, left, right);
        while (k != (bigK ? p + 1 : length - p)) {
            if (bigK ? k < p + 1 : k > length - p) {
                right = p - 1;
            } else if (bigK ? k > p + 1 : k < length - p) {
                left = p + 1;
            }
            p = partition(arr, left, right);
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (arr[arrIndex] > pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

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


}

代碼的改動是涉及K和P的關係的比較,屬於一個逆勢思維。在一串倒序的數中求第K小的數,例如在7, 6, 5, 5, 4, 3, 3, 2, 1 這組數中6是第2大的數,是第8小的數。有興趣的可以看下,不過重點推薦5.0版本的實現方案。

5.0:

這個版本就是4.0小結中的求第K小的數的第2種解決方案:

public class KthNum {
    public static int k = 2;
    public static boolean bigK = false;

    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int kNum = quickSort(arr);
        System.out.println("kNum=" + kNum);
    }

    public static int quickSort(int arr[]) {
        int length = arr.length;
        if (k <= 0 || k > length) throw new RuntimeException("K值不合理");
        int left = 0, right = length - 1;
        int p = -1;
        while (k != p + 1) {
            if (k < p + 1) {
                right = p - 1;
            } else if (k > p + 1) {
                left = p + 1;
            }
            p = partition(arr, left, right);
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (bigK ? arr[arrIndex] > pivot : arr[arrIndex] < pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

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


}

這裏新增了一個成員變量 bigK 來做標識:true代表求第K大的數;false代表求第K小的數。同時在 partition 方法中的第4行也做了改動:若bigK爲true,則採用倒序;若bigK爲false,則採用升序。這樣就以最小的改動實現了求第K大的數和第K小的數的需求,完美。

 

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