先說結論,最終版本代碼如下:
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,使用下標+1和K進行比較,若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小的數的需求,完美。