左神算法进阶班1_5BFPRT算法

在无序数组中找到第k大的数
1)分组,每N个数一组,(一般5个一组)
2)每组分别进行排序,组间不排序
3)将每个组的中位数拿出来,若偶数,则拿上 / 下中位数, 成立一个一个新数组。
4)新数组递归调用BFPRT,则拿到整体的中位数num
5)以num来划分整体数组,小于在左,大于在右边,使用【荷兰国旗方法】
6)然后根据左右数组的规模,来确定进一步选择左右哪一部分;
7)然后选择好后,继续

一:背景介绍
在一大堆数中求其前k大或前k小的问题,简称TOP - K问题。而目前解决TOP - K问题最有效的算法即是BFPRT算法,其又称为中位数的中位数算法,该算法由Blum、Floyd、Pratt、Rivest、Tarjan提出,最坏时间复杂度为O(n)O(n)。

在首次接触TOP - K问题时,我们的第一反应就是可以先对所有数据进行一次排序,然后取其前k即可,但是这么做有两个问题:
(1):快速排序的平均复杂度为O(nlogn),但最坏时间复杂度为O(n2),不能始终保证较好的复杂度。
(2):我们只需要前k大的,而对其余不需要的数也进行了排序,浪费了大量排序时间。

除这种方法之外,堆排序也是一个比较好的选择,可以维护一个大小为k的堆,时间复杂度为O(nlogk)。

那是否还存在更有效的方法呢?受到快速排序的启发,通过修改快速排序中主元的选取方法可以降低快速排序在最坏情况下的时间复杂度(即BFPRT算法),并且我们的目的只是求出前k,故递归的规模变小,速度也随之提高。下面来简单回顾下快速排序的过程,以升序为例:
(1):选取主元(首元素,尾元素或一个随机元素);
(2):以选取的主元为分界点,把小于主元的放在左边,大于主元的放在右边;
(3):分别对左边和右边进行递归,重复上述过程。

二:BFPRT算法过程及代码
BFPRT算法步骤如下:
(1):选取主元;
(1.1):将n个元素划分为[n / 5]个组,每组5个元素,若有剩余,舍去;
(1.2):使用插入排序找到[n / 5]个组中每一组的中位数;
(1.3):对于(1.2)中找到的所有中位数,调用BFPRT算法求出它们的中位数,作为主元;
(2):以(1.3)选取的主元为分界点,把小于主元的放在左边,大于主元的放在右边;
(3):判断主元的位置与k的大小,有选择的对左边或右边递归。//即根据K的位置判断选择哪一部分继续迭代

上面的描述可能并不易理解,先看下面这幅图:
BFPRT()调用GetPivotIndex()和Partition()来求解第k小,在这过程中,GetPivotIndex()也调用了BFPRT(),即GetPivotIndex)和BFPRT()为互递归的关系。

下面为代码实现,其所求为前K小的数:

C++代码:

 1 /******C++**********/
 2 #include<iostream>
 3 #include<algorithm>
 4 using namespace std;
 5
 6 int InsertSort(int  array[], int left, int right);                 //插入排序,返回中位数下标
 7 int GetPivotIndex(int  array[], int left, int right);              //返回中位数的中位数下标
 8 int Partition(int array[], int left, int right, int pivot_index);  //利用中位数的中位数的下标进行划分,返回分界线下标
 9 int BFPRT(int array[], int left, int right, const int & k);        //求第k小,返回其位置的下标
10
11 int main()
12 {
13     int k = 5;
14     int array[10] = { 1,1,2,3,1,5,-1,7,8,-10 };
15
16     cout << "原数组:";
17     for (int i = 0; i < 10; i++)
18         cout << array[i] << " ";
19     cout << endl;
20
21     cout << "第" << k << "小值为:" << array[BFPRT(array, 0, 9, k)] << endl;
22
23     cout << "变换后的数组:";
24     for (int i = 0; i < 10; i++)
25         cout << array[i] << " ";
26     cout << endl;
27
28     return 0;
29 }
30
31 /* 插入排序,返回中位数下标 */
32 int InsertSort(int array[], int left, int right)
33 {
34     int temp;
35     int j;
36     for (int i = left + 1; i <= right; i++)
37     {
38         temp = array[i];
39         j = i - 1;
40         while (j >= left && array[j] > temp)
41             array[j + 1] = array[j--];
42         array[j + 1] = temp;
43     }
44
45     return ((right - left) >> 1) + left;
46 }
47
48 /* 返回中位数的中位数下标 */
49 int GetPivotIndex(int array[], int left, int right)
50 {
51     if (right - left < 5)
52         return InsertSort(array, left, right);
53
54     int sub_right = left - 1;
55     for (int i = left; i + 4 <= right; i += 5)
56     {
57         int index = InsertSort(array, i, i + 4);  //找到五个元素的中位数的下标
58         swap(array[++sub_right], array[index]);   //依次放在左侧
59     }
60
61     return BFPRT(array, left, sub_right, ((sub_right - left + 1) >> 1) + 1);
62 }
63
64 /* 利用中位数的中位数的下标进行划分,返回分界线下标 */
65 int Partition(int array[], int left, int right, int pivot_index)
66 {
67     swap(array[pivot_index], array[right]);  //把基准放置于末尾
68
69     int divide_index = left;  //跟踪划分的分界线
70     for (int i = left; i < right; i++)
71     {
72         if (array[i] < array[right])
73             swap(array[divide_index++], array[i]);  //比基准小的都放在左侧
74     }
75
76     swap(array[divide_index], array[right]);  //最后把基准换回来
77     return divide_index;
78 }
79
80 int BFPRT(int array[], int left, int right, const int & k)
81 {
82     int pivot_index = GetPivotIndex(array, left, right);            //得到中位数的中位数下标
83     int divide_index = Partition(array, left, right, pivot_index);  //进行划分,返回划分边界
84     int num = divide_index - left + 1;
85     if (num == k)
86         return divide_index;
87     else if (num > k)
88         return BFPRT(array, left, divide_index - 1, k);
89     else
90         return BFPRT(array, divide_index + 1, right, k - num);
91 }

Java代码:

  1 public class BFPRT {
  2     //前k小
  3     public static int[] getMinKNumsByBFPRT(int[] arr, int k) {
  4         if (k < 1 || k > arr.length) {
  5             return arr;
  6         }
  7         int minKth = getMinKthByBFPRT(arr, k);
  8         int[] res = new int[k];
  9         int index = 0;
 10         for (int i = 0; i != arr.length; i++) {
 11             if (arr[i] < minKth) {
 12                 res[index++] = arr[i];
 13             }
 14         }
 15         for (; index != res.length; index++) {
 16             res[index] = minKth;
 17         }
 18         return res;
 19     }
 20     //第k小
 21     public static int getMinKthByBFPRT(int[] arr, int K) {
 22         int[] copyArr = copyArray(arr);
 23         return select(copyArr, 0, copyArr.length - 1, K - 1);
 24     }
 25
 26     public static int[] copyArray(int[] arr) {
 27         int[] res = new int[arr.length];
 28         for (int i = 0; i != res.length; i++) {
 29             res[i] = arr[i];
 30         }
 31         return res;
 32     }
 33     //给定一个数组和范围,求第i小的数
 34     public static int select(int[] arr, int begin, int end, int i) {
 35         if (begin == end) {
 36             return arr[begin];
 37         }
 38         int pivot = medianOfMedians(arr, begin, end);//划分值
 39         int[] pivotRange = partition(arr, begin, end, pivot);
 40         if (i >= pivotRange[0] && i <= pivotRange[1]) {
 41             return arr[i];
 42         }
 43         else if (i < pivotRange[0]) {
 44             return select(arr, begin, pivotRange[0] - 1, i);
 45         }
 46         else {
 47             return select(arr, pivotRange[1] + 1, end, i);
 48         }
 49     }
 50     //在begin end范围内进行操作
 51     public static int medianOfMedians(int[] arr, int begin, int end) {
 52         int num = end - begin + 1;
 53         int offset = num % 5 == 0 ? 0 : 1;//最后一组的情况
 54         int[] mArr = new int[num / 5 + offset];//中位数组成的数组
 55         for (int i = 0; i < mArr.length; i++) {
 56             int beginI = begin + i * 5;
 57             int endI = beginI + 4;
 58             mArr[i] = getMedian(arr, beginI, Math.min(end, endI));
 59         }
 60         return select(mArr, 0, mArr.length - 1, mArr.length / 2);
 61         //只不过i等于长度一半,用来求中位数
 62     }
 63     //经典partition过程
 64     public static int[] partition(int[] arr, int begin, int end, int pivotValue) {
 65         int small = begin - 1;
 66         int cur = begin;
 67         int big = end + 1;
 68         while (cur != big) {
 69             if (arr[cur] < pivotValue) {
 70                 swap(arr, ++small, cur++);
 71             }
 72             else if (arr[cur] > pivotValue) {
 73                 swap(arr, cur, --big);
 74             }
 75             else {
 76                 cur++;
 77             }
 78         }
 79         int[] range = new int[2];
 80         range[0] = small + 1;
 81         range[1] = big - 1;
 82         return range;
 83     }
 84     //五个数排序,返回中位数
 85     public static int getMedian(int[] arr, int begin, int end) {
 86         insertionSort(arr, begin, end);
 87         int sum = end + begin;
 88         int mid = (sum / 2) + (sum % 2);
 89         return arr[mid];
 90     }
 91     //手写排序
 92     public static void insertionSort(int[] arr, int begin, int end) {
 93         for (int i = begin + 1; i != end + 1; i++) {
 94             for (int j = i; j != begin; j--) {
 95                 if (arr[j - 1] > arr[j]) {
 96                     swap(arr, j - 1, j);
 97                 }
 98                 else {
 99                     break;
100                 }
101             }
102         }
103     }
104     //交换值
105     public static void swap(int[] arr, int index1, int index2) {
106         int tmp = arr[index1];
107         arr[index1] = arr[index2];
108         arr[index2] = tmp;
109     }
110     //打印
111     public static void printArray(int[] arr) {
112         for (int i = 0; i != arr.length; i++) {
113             System.out.print(arr[i] + " ");
114         }
115         System.out.println();
116     }
117
118     public static void main(String[] args) {
119         int[] arr = { 6, 9, 1, 3, 1, 2, 2, 5, 6, 1, 3, 5, 9, 7, 2, 5, 6, 1, 9 };
120         // sorted : { 1, 1, 1, 1, 2, 2, 2, 3, 3, 5, 5, 5, 6, 6, 6, 7, 9, 9, 9 }
121         printArray(getMinKNumsByBFPRT(arr, 10));
122
123     }
124 }
125  

原文地址:https://www.cnblogs.com/zzw1024/p/11037421.html

时间: 2024-07-31 22:33:15

左神算法进阶班1_5BFPRT算法的相关文章

左神算法进阶班1_1添加最少字符得到原字符N次

Problem: 给定一个字符串str1,只能往str1的后面添加字符变成str2. 要求1:str2必须包含两个str1,两个str1可以有重合,但是不能以同一个位置开头. 要求2:str2尽量短最终返回str2 举例: str1 = 123,str2 = 123123 时,包含两个str1,且不以相同位置开头,且str2最短. str1 = 123123,str2 = 123123123 时,包含两个str1,且不以相同位置开头,且str2最短. str1 = 111,str2 = 1111

左神算法进阶班3_1构造数组的MaxTree

题目 一个数组的MaxTree定义: 数组必须没有重复元素 MaxTree是一棵二叉树,数组的每一个值对应一个二叉树节点 包括MaxTree树在内且在其中的每一棵子树上,值最大的节点都是树的头 给定一个没有重复元素的数组arr,写出生成这个数组的MaxTree的函数,要求如果数组长度为N,则时间负责度为O(N).额外空间负责度为O(N). 实现思路 将数组按照大根堆进行排序 然后直接按照大根堆进行构造一颗二叉树即可. 使用单调栈 通过使用单调栈,将数组中中所有数的左右比他大的数记录下来 当某个数

左神算法进阶班5_1求二叉树中最大搜索子树大小

[题目] 给定一棵二叉树的头节点head,请返回最大搜索二叉子树的大小 [题解] 简化问题,想到该问题的解答应该有几种情形 第一种可能: 最大搜索二叉子树在head的左子树 第二种可能: 最大搜索二叉子树在head的右子树 第三种可能: 最大搜索二叉子树为自己:利用左子树的最大值与右子树的最小值 递归左子树,再递归右子树 信息1:左子树中最大搜索二叉树的大小 信息2:右子树中最大搜索二叉树的大小 信息3:左子树最大搜索二叉树的头结点 信息4:右子树最大搜索二叉树的头结点 信息5:左子树上的最大值

左神算法进阶班6_1LFU缓存实现

[题目] LFU也是一个著名的缓存算法,自行了解之后实现LFU中的set 和 get 要求:两个方法的时间复杂度都为O(1) [题解] LFU算法与LRU算法很像 但LRU是最新使用的排在使用频率最前面,也就是LRU是通过使用时间进行排序, 使用时间越新,其使用频率越高,而使用时间越久,其使用频率越低,即当空间满时,被删除的概率最大 而LFU是根据使用次数来算使用频率的,使用次数越多,其使用频率越高,使用次数越少,使用频率越低,当空间满时越容易被删除 同样,使用hash_map表和双向链表进行存

左神算法进阶班5_3求公司的最大活跃度

[题目] 一个公司的上下节关系是一棵多叉树,这个公司要举办晚会,你作为组织者已经摸清了大家的心理: 一个员工的直接上级如果到场,这个员工肯定不会来. 每个员工都有一个活跃度的值,决定谁来你会给这个员工发邀请函,怎么让舞会的气氛最活跃? 返回最大的活跃值. 举例: 给定一个矩阵来表述这种关系 matrix = { 1,6 1,5 1,4 } 这个矩阵的含义是: matrix[0] = { 1 , 6 },表示0这个员工的直接上级为1, 0这个员工自己的活跃度为6 matrix[1] = { 1 ,

左神算法基础班3_13深度拷贝含有随机指针的链表

Problem: 复制含有随机指针节点的链表 [题目] 一种特殊的链表节点类描述如下: public class Node { public int value; public Node next; public Node rand; public Node(int data) { this.value = data; } } Node类中的value是节点值,next指针和正常单链表中next指针的意义 一 样,都指向下一个节点,rand指针是Node类中新增的指针,这个指 针可 能指向链表中

左神算法书籍《程序员代码面试指南》——2_07将单向链表按某值划分成左边小、中间相等、右边大的形式

Problem:[题目] 给定一个单向链表的头节点head,节点的值类型是整型,再给定一个整数pivot. 实现一个调整链表的函数,将链表调整为左部分都是值小于 pivot的节点, 中间部分都是值等于pivot的节点,右部分都是值大于 pivot的节点. 除这个要求外,对调整后的节点顺序没有更多的要求. 例如:链表9->0->4->5->1,pivot = 3. 调整后链表可以是1->0->4->9->5, 可以是0->1->9->5-&g

左神算法第八节课:介绍递归和动态规划(汉诺塔问题;打印字符串的全部子序列含空;打印字符串的全排列,无重复排列;母牛数量;递归栈;数组的最小路径和;数组累加和问题,一定条件下最大值问题(01背包))

暴力递归: 1,把问题转化为规模缩小了的同类问题的子问题 2,有明确的不需要继续进行递归的条件(base case) 3,有当得到了子问题的结果之后的决策过程 4,不记录每一个子问题的解 动态规划 1,从暴力递归中来 2,将每一个子问题的解记录下来,避免重复计算 3,把暴力递归的过程,抽象成了状态表达 4,并且存在化简状态表达,使其更加简洁的可能 一:递归 1. 汉诺塔问题 汉诺塔问题(不能大压小,只能小压大),打印n层汉诺塔从最左边移动到最右边的全部过程. 左中右另称为 from.to.hel

【左神算法课】子数组最大差值小于某阈值,求满足条件的子数组个数

题目描述: 解法思路: 本题其实是滑动窗口的变形.主体思路为: 1.从第一个元素开始依次向后遍历,同时维护两个窗口(由于要同时操作窗口的头部和尾部,故采用双端队列): 最大值窗口(递减),头部永远存最大值 最小值窗口(递增),头部永远存最小值 2.比较两个窗口的头部元素差值,若差值大于阈值,即可跳出内循环. 3.跳出内循环后,检查头部元素是否过期,若过期,则清除. 复杂度: 时间复杂度:O(n),注意虽然是两层循环,但元素只从滑动窗口尾部进,从头部清除,只是顺序扫描了一遍. 空间复杂度:O(n)