一个无序数组中的第 k 小
堆
整体思路,维护一个大根堆,遍历每个数的时候,判断它是不是比堆顶小,如果小的话,就删除堆顶的,并把当前值添加进去。最后堆顶即为第 k 小。总体时间复杂度为 O ( N l o g k ) O(Nlogk) O(Nlogk),空间复杂度为 O ( k ) O(k) O(k)
// public static int findKthLargest(int[] nums, int k) {
// if (nums == null || nums.length == 0 || k > nums.length) return -1;/
// return getKthMin(nums, nums.length - k + 1);
// }
//
// 新添加的数 在 i 位置,进行堆化
public static void heapInsert(int[] heap, int i) {
while(heap[(i - 1) / 2] < heap[i]) {
swap(heap, i, (i-1)/2);
i = (i - 1) / 2;
}
}
// i 位置的值,发生了变化,重新调整堆
public static void heapify(int[] heap, int i) {
int left = 2 * i + 1;
int n = heap.length;
while (left < n) {
// 找到孩子中的较大的那个索引值
int largestIndex = (left + 1 < n) && heap[left + 1] > heap[left] ? left + 1:left;
// 父亲已经大于孩子了
if (heap[i] > heap[largestIndex]) {
break;
}
swap(heap, i, largestIndex);
i = largestIndex;
left = 2 * i + 1;
}
}
// 堆得到 第 k 小的数
// 维护一个大根堆
public static int getKthMin(int[] nums, int k) {
int[] heap = new int[k];
// for (int i = k-1; i >= 0; i--) {
// heap[i] = nums[i];
// heapify(heap, i);
// }
for (int i = 0; i < k; i++) {
heap[i] = nums[i];
heapInsert(heap, i);
}
for (int i = k; i < nums.length; i++) {
/// 当前这个数可以添加进去
if (nums[i] < heap[0]) {
heap[0] = nums[i];
heapify(heap, 0);
}
}
return heap[0];
}
public static void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
快排思路
标准的快排程序是每次选择一个基准值,然后通过这个基准值调 partition 程序,将源数组分为 3 部分,并且返回中间部分的起始和结束坐标。当 k 在这个坐标范围内时,就得到了第 k 小的值。此方法时间复杂度是
O
(
N
)
O(N)
O(N)的,不过是基于概率的 O(N),因为每次选择的基准值是随机选择的,可能情况下排除掉最多元素,可能情况下一个也排除不了。
public int getKthSmallest(int[] nums, int k) {
int l = 0, r = nums.length - 1;
while (l <= r) {
// 在 l...r 上随机选择一个值,作为基准值
int pivot = nums[l + (int) (Math.random() * (r - l + 1))];
int[] p = partition(nums, l, r, pivot);
if (k - 1 >= p[0] && k - 1 <= p[1]) {
return nums[p[0]];
}
if (k - 1 < p[0]) {
r = p[0] - 1;
}
if (k - 1 > p[1]) {
l = p[1] + 1;
}
}
return -1;
}
public int[] partition(int[] nums, int l, int r, int pivot) {
int less = l - 1, more = r + 1;
while (l < more) {
if (nums[l] < pivot) {
swap(nums, ++less, l++);
} else if (nums[l] > pivot) {
swap(nums, --more, l);
} else {
l++;
}
}
return new int[]{less + 1, more - 1};
}
BFPRT
此方法和快排的区别在于选取基准值的不同,复杂度也是O(N)的,不过是确定性的O(N),因为其选取基准值是通过了一定的方法,并且可以保证每次至少排除 3 / 10 N 3/10N 3/10N 的数据。
//arr L...R 位置上,如果排序的话,返回 index位置的数。
public static int bfprt(int[] arr, int L, int R, int index) {
if (L == R) {
return arr[L];
}
// 数组 L...R 中每 5 个数作为一组,找到组内的中位数,这些中位数在形成一个数组,找到这个数组的中位数,构成基准值。
int pivot = mOfM(arr, L, R);
int[] range = partition(arr, L, R, pivot);
if (index >= range[0] && index <= range[1]) {
return arr[index];
} else if (index-1 < range[0]) {
return bfprt(arr, L, range[0] - 1, index);
} else {
return bfprt(arr, range[1] + 1, R, index);
}
}
public static int mOfM(int[] arr, int L, int R) {
int size = R - L + 1;
int offset = size % 5 == 0 ? 0 : 1;
int[] mArr = new int[size / 5 + offset];
for (int team = 0; team < mArr.length; team++) {
int teamFirst = L + team * 5;
mArr[team] = getMedian(arr, teamFirst, Math.min(R, teamFirst + 4));
}
// marr中,找到中位数
// marr(0, marr.len - 1, mArr.length / 2 )
return bfprt(mArr, 0, mArr.length - 1, mArr.length / 2);
}
public static int getMedian(int[] arr, int L, int R) {
insertionSort(arr, L, R);
return arr[(L + R) / 2];
}
public static void insertionSort(int[] arr, int L, int R) {
for (int i = L + 1; i <= R; i++) {
for (int j = i - 1; j >= L && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
两个有序数组中的第 k 小
整体思路呢利用两个数组的有序性和k的取值范围,一次性排除掉一批数,底层利用求两个等长有序数组的上中位数的操作,算法复杂度达到了 O ( l o g ( m i n ( m , n ) ) ) O(log(min(m, n))) O(log(min(m,n)))
// 得到两个有序数组中的第 k 小的数
// 基本原型,得到两个等长有序数组的上中位数
public static int getKthSmallest(int[] nums1, int[] nums2, int k) {
// 无效的 k
// if (k > (nums1.length + nums2.length)) return -1;
int[] longs = nums1.length >= nums2.length ? nums1 : nums2;
int[] shorts = longs == nums1 ? nums2 : nums1;
int l = longs.length, s = shorts.length;
// 根据 k 的范围来进行划分
// p1: k 小于等于 短数组长度
if (k <= s) {
return getUpMedian(shorts, 0, k - 1, longs, 0, k - 1);
}
// p2: k 小于等于 长数组长度
if (k <= l) {
if (longs[k - s - 1] >= shorts[s - 1]) {
return longs[k - s - 1];
}
// 短的要全部用上了,肯定是 s - 1了
return getUpMedian(shorts, 0, s - 1, longs, k - s, k - 1);
}
// p3: k 大于长数组长度
if (shorts[k - l - 1] >= longs[l - 1]) {
return shorts[k - l - 1];
}
if (longs[k - s - 1] >= shorts[s - 1]) {
return longs[k - l - 1];
}
return getUpMedian(shorts, k - l, s - 1, longs, k - s, l - 1);
}
// 分为奇数和偶数两种情况
// 保证输入的两个边界范围是有效的,并且等长的
public static int getUpMedian(int[] A, int l1, int r1, int[] B, int l2, int r2) {
while (l1 < r1) {
int mid1 = (l1 + r1) / 2;
int mid2 = (l2 + r2) / 2;
// 两个mid 相等,说明 就是上中位数了
if (A[mid1] == B[mid2]) {
return A[mid1];
}
if ((((r1 - l1 + 1) & 1)) == 0) {
// 如果长度是偶数的话
if (A[mid1] > B[mid2]) {
r1 = mid1;
l2 = mid2 + 1;
} else {
r2 = mid2;
l1 = mid1 + 1;
}
} else {
// 那就只能是奇数了
if (A[mid1] > B[mid2]) {
if (B[mid2] >= A[mid1 - 1]) {
return B[mid2];
}
r1 = mid1 - 1;
l2 = mid2 + 1;
} else {
if (A[mid1] >= B[mid2 - 1]) {
return A[mid1];
}
r2 = mid2 - 1;
l1 = mid1 + 1;
}
}
}
return Math.min(A[l1], B[l2]);
}
// 得到两个有序数组的中位数,如果是偶数长度,上中位数+下中位数和除以2。
// public static double findMedianSortedArrays(int[] nums1, int[] nums2) {
// if (nums1 == null && nums2 == null) return -1;
// if ((nums1 == null ^ nums2 == null) || (nums1.length == 0 ^ nums2.length == 0)) {
// int[] temp = (nums1 == null || nums1.length == 0) ? nums2 : nums1;
// if ((temp.length & 1) == 1) {
// return temp[temp.length / 2];
// } else {
// return (temp[temp.length / 2] + temp[temp.length / 2 - 1] )/ 2.0;
// }
// }
// int n1 = nums1.length;
// int n2 = nums2.length;
// int all = n1 + n2;
// if ((all & 1) == 1) {
// return getKthSmallest(nums1, nums2, all / 2 + 1);
// }
// return (getKthSmallest(nums1, nums2, all / 2) + getKthSmallest(nums1, nums2, all/ 2 + 1)) / 2.0;
// }
`