/*
* 存在两个问题:一个是程序没看懂,一个是第二种解法中出现的堆栈溢出的问题
* 堆栈溢出的错误通过举实际的例子即可得到,当begin值不为0时。
* 通过eclipse的调试程序功能可以看到一步步运行,把main()中的arr设置的短一些,然后k值也设置小一点,方便调试。
*
*/
package advanceClassCode;
//BFPRT算法实现在无序数组中找到第k小的数
public class Code_06_BFPRT {
// O(N*logK)堆排序,K个结点组成的树高度是logK,由于只需要将数组arr中的前k个最小的数抽取出来,这K个节点构成的树的高度是logK。heapInsert()的复杂度是O(logK),heapify()的复杂度是O(N),所以总的复杂度是O(NlogK)
public static int[] getMinKNumsByHeap(int[] arr, int k) {
if (k < 1 || k > arr.length) {
return arr;
}
int[] kHeap = new int[k];
for (int i = 0; i != k; i++) {
heapInsert(kHeap, arr[i], i);//前k个结点构成最大根二叉树
}
//把原数组中从下标从K开始的到arr.length-1的下标的范围内的值,若发现有值小于KHeap[0]即之前选出的k个值中的最大值,则把较小的当前arr数组中的值调整到kHeap[0],因为kHeap[]里最后就是保存原数组中最小的k个值。之前KHeap[]中保存的k个值是把原数组中下标从0到k-1的数按照最大根的形式组成了一个二叉树,现在是把数组arr[]中剩下的数中,只要发现比KHeap[]中最大值小的数,就与之交换,在arr[]剩下的值中一个个遍历,直到最后KHeap[]中保存最小的K个数。
for (int i = k; i != arr.length; i++) {
if (arr[i] < kHeap[0]) {
kHeap[0] = arr[i];//后面剩下的结点里若存在比之前构造出的最大根二叉树最大值小,
// 若有则把小值放置到最大根二叉树的根结点上。
//这个函数就是实现当把数组中较小的值与KHeap[0]交换后,重新把KHeap调整为最大根二叉树
heapify(kHeap, 0, k);//把二叉树调整
}
}
return kHeap;
}
//从树的根节点开始构造,即下标为0开始构造,从上往下构建出这棵树,并且遇到后面插入的子节点值大于根节点,就把大值网上调整到根节点处。构造出总结点个数为K的最大根二叉树。
public static void heapInsert(int[] arr, int value, int index) {
arr[index] = value;
while (index != 0) {
int parent = (index - 1) / 2;//父节点
if (arr[parent] < arr[index]) {
swap(arr, parent, index);//插入新节点,大的值就往父节点转移,最大根
index = parent;
} else {
break;
}
}
}
//从index=0开始对传入的数组KHeap[]对应的二叉树进行调整,调整为最大根二叉树。
public static void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1;
int right = index * 2 + 2;
int largest = index;
//一层层往下遍历,把左右儿子中大的节点与其父节点值交换。
//然后还要把当前考虑的下标index改为交换的原本大值的下标,若是与左儿子交换,则index下标改为left下标,若是与右儿子交换,则index改为right下标,然后在往下,再往新的index的左右儿子比较。
while (left < heapSize) {
if (arr[left] > arr[index]) {
largest = left;
}
if (right < heapSize && arr[right] > arr[largest]) {
largest = right;
}
if (largest != index) {
swap(arr, largest, index);
} else {
break;
}
index = largest;
left = index * 2 + 1;
right = index * 2 + 2;
}
}
/*
* 2020/1/13复习一遍
*/
// O(N)
public static int[] getMinKNumsByBFPRT(int[] arr, int k) {
if (k < 1 || k > arr.length) {
return arr;
}
// minKth就是满足题意的数组中第k小的数组值
int minKth = getMinKthByBFPRT(arr, k);//这里调用用的参数k
// 创建大小为k的数组
int[] res = new int[k];
int index = 0;
for (int i = 0; i != arr.length; i++) {
if (arr[i] < minKth) {//遍历数组,挑选出符合条件的数组中所有比第k小的数还
// 要小的数,此时这些数并没有顺序
res[index++] = arr[i];
}
}
// index起始位置是接着上面的index位置,此时把前k个位置中未填满的位置全部填上第k小的那个值
for (; index != res.length; index++) {
res[index] = minKth;
}
return res;
}
public static int getMinKthByBFPRT(int[] arr, int K) {
int[] copyArr = copyArray(arr);
//返回的是满足题意的第k小的数组元素值,也是调用的select的返回值
return select(copyArr, 0, copyArr.length - 1, K - 1);//这里调用用的参数k-1
}
public static int[] copyArray(int[] arr) {
int[] res = new int[arr.length];
for (int i = 0; i != res.length; i++) {
res[i] = arr[i];
}
return res;
}
public static int select(int[] arr, int begin, int end, int i) {
if (begin == end) {
return arr[begin];
}
//pivot是中位数数组里的中位数,不断循环嵌套递归调用,直到分割后剩下的中位数数组中不足5个元素,直接得到其中位数
// 元素即为pivot
// 因为传入select()的参数是begin和end,所以下面也是写begin和end
int pivot = medianOfMedians(arr, begin, end);//求出由中位数构成的数组里的中位数
// 以这个最终的中位数为划分依据,将原数组划分为三部分区域
int[] pivotRange = partition(arr, begin, end, pivot);//调用partition()
// 得到的是等于pivot值区域的左右边界
// 传入的i参数是k-1,k-1位于等于区域
// 看k-1落在三个区域的哪个范围
if (i >= pivotRange[0] && i <= pivotRange[1]) {
// 落在等于区域那么值直接就是pivot
return arr[i];
// k-1位于小于区域,递归调用
} else if (i < pivotRange[0]) {
// 落在小于区域,那么需要将小于区域也partition操作,递归嵌套循环调用
return select(arr, begin, pivotRange[0] - 1, i);
// k-1位于大于区域
} else {
return select(arr, pivotRange[1] + 1, end, i);
}
}
public static int medianOfMedians(int[] arr, int begin, int end) {
int num = end - begin + 1;
// 把数组值按照5为间隔分为若干区域,最后一个区域的数目可能不满5
int offset = num % 5 == 0 ? 0 : 1;
int[] mArr = new int[num / 5 + offset];
for (int i = 0; i < mArr.length; i++) {
// 每个子数组的起始和终止下标,是begin+i*5不要粗心写成了begin*i+5
int beginI = begin + i * 5;
int endI = beginI + 4;
//因为对于最后一个分出的子数组,数量可能不足5,所以需要在end和endI之间取小值
// 计算子数组的中位数
mArr[i] = getMedian(arr, beginI, Math.min(end, endI));
}
// mArr是由中位数构成的新数组,传入select()的参数不是k-1而是新数组的中位数下标,这里是循环嵌套调用,终止的
// 条件是当不断按照以5为区间分割出子数组,并且取中位数构成新数组,当新数组的数组不足5时,就不能继续调用了
// 此时直接就能得到不足5个元素数组的中位数
// 注意end是mArr.length-1而不是mArr.length
return select(mArr, 0, mArr.length - 1, mArr.length / 2);
}
// 借助荷兰国旗问题
public static int[] partition(int[] arr, int begin, int end, int pivotValue) {
// 小于区域
int small = begin - 1;
// 注意这里cur是从begin开始而不是从0开始
int cur = begin;
// 大于区域
int big = end + 1;
// 注意这里的循环范围是不能达到big,而不是不越过数组的界即cur<arr.length
while (cur != big) {
if (arr[cur] < pivotValue) {
swap(arr, ++small, cur++);
} else if (arr[cur] > pivotValue) {
swap(arr, cur, --big);
} else {
cur++;
}
}
int[] range = new int[2];
range[0] = small + 1;
range[1] = big - 1;
return range;//返回等于区域的两个边界下标
}
// 计算给定子数组的中位数
public static int getMedian(int[] arr, int begin, int end) {
insertionSort(arr, begin, end);
// 下面的语句换成int sum=end-begin就会出现statck overflow的错误,
// 调试的时候程序无法结束??????????????????
int sum = end + begin;
// 下面语句用int mid=sum/2代替也可以
int mid = (sum / 2) + (sum % 2);
return arr[mid];
}
// 得到从小到大的升序排列,因为是对分割后的子数组内部排序,子数组间是不需要排序的,所以需要begin和end参数来确定出具体的子数组
public static void insertionSort(int[] arr, int begin, int end) {
// 因为已经传入了参数begin和end,所以下面的范围写i!=end+1,而不是写i<arr.length
for (int i = begin + 1; i != end + 1; i++) {//i要从begin+1开始因为后面j=i,需要j-1
for (int j = i; j != begin; j--) {
if (arr[j - 1] > arr[j]) {
swap(arr, j - 1, j);
} else {
break;//退出本次循环,进行下一轮循环即j--
}
}
}
}
public static void swap(int[] arr, int index1, int index2) {
int tmp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = tmp;
}
public static void printArray(int[] arr) {
for (int i = 0; i != arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
public static void main(String[] args) {
int[] arr = { 6, 9, 1, 3, 1, 2, 2, 5, 6, 1, 3, 5, 9, 7, 2, 5, 6, 1, 9 };
// sorted : { 1, 1, 1, 1, 2, 2, 2, 3, 3, 5, 5, 5, 6, 6, 6, 7, 9, 9, 9 }
// 得到数组中前k小的数,输出这符合要求的K个数是没有顺序的
printArray(getMinKNumsByHeap(arr, 10));
printArray(getMinKNumsByBFPRT(arr, 10));
// 若想输出数组,不能采用System.out.print(getMinKNumsByBFPRT(arr,10));
}
}
程序上遇到的问题总结
最新推荐文章于 2022-07-02 17:21:56 发布