今天我通过一道简单题目,帮助大家了解一下选择排序,堆排序
题目描述:
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
题目示例:
选择排序
刚开始拿到这道题目,我首先想到的就是 选择排序,不是很了解选择 排序的可以先看一下:《排序算法—选择排序》
本题通过选择排序的代码如下:
public int[] getLeastNumbers(int[] arr, int k) {
int length = arr.length;
if (length <= 0) return arr;
return selectSort(arr, k);
}
public int[] selectSort(int[] nums, int k) {
int[] re = new int[k];
int length = nums.length;
int count = 0;
for (int i = 0; i < length - 1; i++) {
int min = i;
for (int j = i + 1; j < length; j++) {
if (nums[min] > nums[j]) min = j;
}
if (count < k) {
re[count++] = nums[min];
if (min != i) {
int t = nums[i];
nums[i] = nums[min];
nums[min] = t;
}
} else return re;
}
if (count < k) {
re[count] = nums[count];
}
return re;
}
后来发现执行起来特别慢:
快速排序
然后我又开始尝试快速排序,快速排序在另一篇文章里从原理到实现讲的很详细,不会的可以看一下:《排序算法—快速排序》
然后我们直接看看本题快速排序的实现,先看看最简单的思路,直接快排所有元素,然后返回前k个元素
public int[] getLeastNumbers(int[] arr, int k) {
int length = arr.length;
if (length <= 0) return arr;
int[] re = new int[k];
quickSort(arr, 0, length-1);
for (int i = 0; i < k; i++) {
re[i] = arr[i];
}
return re;
}
public void quickSort(int[] nums, int start, int end) {
if (start >= end) return;
int left = start;
int right = end;
int privot = nums[left];
while (left < right) {
while (left < right && nums[right] >= privot) {
right--;
}
nums[left] = nums[right];
while (left < right && nums[left] <= privot) {
left++;
}
nums[right] = nums[left];
}
nums[left] = privot;
quickSort(nums, start, left - 1);
quickSort(nums, left + 1, end);
}
速度明显提升,但是通过快排所有元素,太过于浪费,我们找前 K 大/前 K 小问题不需要对整个数组进行 O(NlogN) 的排序
直接通过快排切分排好第 K 小的数(下标为 K-1),那么它左边的数就是比它小的另外 K-1 个数啦~
再通俗点就是:我们需要划分的下标如:nums= {3, 2, 1, 3, 30, 34, 5, 9};k=2
那么我们第一次划分的结果为:{1,2,3,3,30,34,5,9},划分的边界值 为 (即left的值):2
由于left左边的一定小于left右边的,所以我们只需要继续划分左边的即可,于是最终代码如下:
public int[] getLeastNumbers(int[] arr, int k) {
int length = arr.length;
if (length <= 0) return arr;
int[] re = new int[k];
quickSort(arr, 0, length - 1, k);
for (int i = 0; i < k; i++) {
re[i] = arr[i];
}
return re;
}
public void quickSort(int[] nums, int start, int end, int k) {
if (start >= end || start >= k) return;
int left = start;
int right = end;
int privot = nums[left];
while (left < right) {
while (left < right && nums[right] >= privot) {
right--;
}
nums[left] = nums[right];
while (left < right && nums[left] <= privot) {
left++;
}
nums[right] = nums[left];
}
nums[left] = privot;
if (left >= k) {//如果left大于k说明左边已经满足
quickSort(nums, start, left - 1, k);
}
if (left < k) {
quickSort(nums, left + 1, end, k);
}
}
可以看到效率又提升了一倍
堆排序
最后我们再看一下 堆排序,《排序算法—堆排序》
本题由于是实现最小的k个数, 所以我们通过小顶堆实现,代码如下:
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
int length = arr.length;
if (length <= 0) return arr;
int[] re = heapSort(arr, k);
return re;
}
/**
* 堆排序
*
* @param arr
*/
public static int[] heapSort(int[] arr, int k) {
int j = 0;//re下标
int[] re = new int[k];
if (arr == null || arr.length == 0 || k <= 0) {
return re;
}
int len = arr.length;
// 构建大顶堆,这里其实就是把待排序序列,变成一个大顶堆结构的数组
buildMaxHeap(arr, len);
// 交换堆顶和当前末尾的节点,重置大顶堆
for (int i = len - 1; i > 0; i--) {
swap(arr, 0, i);//交换堆顶元素和堆尾元素的值
re[j++] = arr[i];
if (j < k) {
len--;
HeadAdjust(arr, 0, len);
} else break;
}
if (j < k) {
re[j] = arr[0];
}
return re;
}
private static void buildMaxHeap(int[] arr, int len) {
// 从最后一个非叶节点开始向前遍历,调整节点性质,使之成为大顶堆
for (int i = len / 2 - 1; i >= 0; i--) {//从i=length/2-1
HeadAdjust(arr, i, len);
}
}
private static void HeadAdjust(int[] arr, int i, int len) {
// 先根据堆性质,找出它左右节点的索引
int left = 2 * i + 1;
int right = 2 * i + 2;
// 默认当前节点(父节点)是最大值。
int largestIndex = i;
if (left < len && arr[left] < arr[largestIndex]) {
// 如果有左节点,并且左节点的值更小,更新最小值的索引
largestIndex = left;
}
if (right < len && arr[right] < arr[largestIndex]) {
// 如果有右节点,并且右节点的值更大,更新最大值的索引
largestIndex = right;
}
if (largestIndex != i) {
// 如果最大值不是当前非叶子节点的值,那么就把当前节点和最大值的子节点值互换
swap(arr, i, largestIndex);
// 因为互换之后,子节点的值变了,如果该子节点也有自己的子节点,仍需要再次调整。
HeadAdjust(arr, largestIndex, len);
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
可以发现效率一般,但是比选择排序效率要高