寻找第K大的数的方法总结

原文地址:http://www.cnblogs.com/zhjp11/archive/2010/02/26/1674227.html

今天看算法分析是,看到一个这样的问题,就是在一堆数据中查找到第k个大的值。

      名称是:设计一组N个数,确定其中第k个最大值,这是一个选择问题,当然,解决这个问题的方法很多,本人在网上搜索了一番,查找到以下的方式,决定很好,推荐给大家。

      所谓“第(前)k大数问题”指的是在长度为n(n>=k)的乱序数组中S找出从大到小顺序的第(前)k个数的问题。

      解法1: 我们可以对这个乱序数组按照从大到小先行排序,然后取出前k大,总的时间复杂度为O(n*logn + k)。
      解法2: 利用选择排序或交互排序,K次选择后即可得到第k大的数。总的时间复杂度为O(n*k)
      解法3: 利用快速排序的思想,从数组S中随机找出一个元素X,把数组分为两部分Sa和Sb。Sa中的元素大于等于X,Sb中元素小于X。这时有两种情况:
           1. Sa中元素的个数小于k,则Sb中的第k-|Sa|个元素即为第k大数;
           2. Sa中元素的个数大于等于k,则返回Sa中的第k大数。时间复杂度近似为O(n)
      解法4: 二分[Smin,Smax]查找结果X,统计X在数组中出现,且整个数组中比X大的数目为k-1的数即为第k大数。时间复杂度平均情况为O(n*logn)
      解法5:用O(4*n)的方法对原数组建最大堆,然后pop出k次即可。时间复杂度为O(4*n + k*logn)
      解法6:维护一个k大小的最小堆,对于数组中的每一个元素判断与堆顶的大小,若堆顶较大,则不管,否则,弹出堆顶,将当前值插入到堆中。时间复杂度O(n * logk)
      解法7:利用hash保存数组中元素Si出现的次数,利用计数排序的思想,线性从大到小扫描过程中,前面有k-1个数则为第k大数,平均情况下时间复杂度O(n)

      附注:
      1. STL中可以用nth_element求得类似的第n大的数(由谓词决定),使用的是解法3中的思想,还可以用partial_sort对区间进行部分排序,得到类似前k大的数(由谓词决定),它采用的是解法5的思想。
      2. 求中位数实际上是第k大数的特例。
          《编程之美》2.5节课后习题:
           1. 如果需要找出N个数中最大的K个不同的浮点数呢?比如,含有10个浮点数的数组(1.5,1.5,2.5,3.5,3.5,5,0,- 1.5,3.5)中最大的3个不同的浮点数是(5,3.5,2.5)。
           解答:上面的解法均适用,需要注意的是浮点数比较时和整数不同,另外求hashkey的方法也会略有不同。
           2. 如果是找第k到第m(0<k<=m<=n)大的数呢?
           解答:如果把问题看做m-k+1个第k大问题,则前面解法均适用。但是对于类似前k大这样的问题,最好使用解法5或者解法7,总体复杂度较低。
       3. 在搜索引擎中,网络上的每个网页都有“权威性”权重,如page rank。如果我们需要寻找权重最大的K个网页,而网页的权重会不断地更新,那么算法要如何变动以达到快速更新(incremental update)并及时返回权重最大的K个网页?
提示:堆排序?当每一个网页权重更新的时候,更新堆。还有更好的方法吗?
       解答:要达到快速的更新,我们可以解法5,使用映射二分堆,可以使更新的操作达到O(logn)

       4. 在实际应用中,还有一个“精确度”的问题。我们可能并不需要返回严格意义上的最大的K个元素,在边界位置允许出现一些误差。当用户输入一个query的时候,对于每一个文档d来说,它跟这个query之间都有一个相关性衡量权重f (query, d)。搜索引擎需要返回给用户的就是相关性权重最大的K个网页。如果每页10个网页,用户不会关心第1000页开外搜索结果的“精确度”,稍有误差是可以接受的。比如我们可以返回相关性第10 001大的网页,而不是第9999大的。在这种情况下,算法该如何改进才能更快更有效率呢?网页的数目可能大到一台机器无法容纳得下,这时怎么办呢?

      提示:归并排序?如果每台机器都返回最相关的K个文档,那么所有机器上最相关K个文档的并集肯定包含全集中最相关的K个文档。由于边界情况并不需要非常精确,如果每台机器返回最好的K’个文档,那么K’应该如何取值,以达到我们返回最相关的90%*K个文档是完全精确的,或者最终返回的最相关的K个文档精确度超过90%(最相关的K个文档中90%以上在全集中相关性的确排在前K),或者最终返回的最相关的K个文档最差的相关性排序没有超出110%*K。
      解答:正如提示中所说,可以让每台机器返回最相关的K'个文档,然后利用归并排序的思想,得到所有文档中最相关的K个。 最好的情况是这K个文档在所有机器中平均分布,这时每台机器只要K' = K / n (n为所有机器总数);最坏情况,所有最相关的K个文档只出现在其中的某一台机器上,这时K'需近似等于K了。我觉得比较好的做法可以在每台机器上维护一个堆,然后对堆顶元素实行归并排序。

       5. 如第4点所说,对于每个文档d,相对于不同的关键字q1, q2, …, qm,分别有相关性权重f(d, q1),f(d, q2), …, f(d, qm)。如果用户输入关键字qi之后,我们已经获得了最相关的K个文档,而已知关键字qj跟关键字qi相似,文档跟这两个关键字的权重大小比较靠近,那么关键字qi的最相关的K个文档,对寻找qj最相关的K个文档有没有帮助呢?

解答:肯定是有帮助的。在搜索关键字qj最相关的K个文档时,可以在qj的“近义词”相关文档中搜索部分,然后在全局的所有文档中在搜索部分。

<think>好的,我现在要解决的问题是用C语言实现寻找二叉搜索树的第k元素。首先,我需要明确什么是二叉搜索树(BST)。二叉搜索树的特点是,每个节点的左子树所有节点的值都小于该节点的值,而右子树所有节点的值都于该节点的值。这种结构对于查找、插入、删除操作非常高效。那问题来了,如何找到第k的元素呢? 首先,我应该回顾一下二叉搜索树的性质。通常,中序遍历二叉搜索树会得到一个递增的序列。例如,按照左-根-右的顺序遍历,会从小到访问所有节点。那如果要找第k的元素,是不是可以通过逆中序遍历,也就是右-根-左的顺序,这样访问的顺序就是从到小了。这样的话,只需要在遍历的时候记录当前是第几个节点,当计等于k时,就可以得到第k的元素了。 接下来,我需要考虑如何实现逆中序遍历。常规的中序遍历可以用递归或者迭代的方式实现,而逆中序遍历则需要先访问右子树,再访问根节点,最后访问左子树。递归的方法可能比较容易实现,但需要注意递归的深度,防止栈溢出。不过,对于一般的二叉搜索树来说,递归应该还是可行的,除非树的高度非常。 那具体步骤应该怎么设计呢?首先,我需要一个计器来记录当前访问到第几个节点。当进行逆中序遍历时,每访问一个节点,计器加1。当计器等于k时,记录该节点的值,并且可以提前终止遍历,避免不必要的访问。这样可以提高效率,特别是在k比较小的时候。 接下来考虑函数的参和返回值。主函数可能需要传入二叉树的根节点和k的值,然后返回第k的节点的值。在递归过程中,需要一个全局变量或者引用传递的变量来保存计器和结果。不过,在C语言中没有引用传递,所以可能需要使用静态变量或者指针来修改计器和结果的值。 这里需要注意的是,递归函数中如果使用静态变量的话,多次调用可能会有问题,因为静态变量的值在程序运行期间会保持。所以,可能需要每次调用查找函数前重置这些静态变量。或者,更好的方式是使用辅助函数,并将计器和结果作为指针参传递进去。 另外,还要处理一些边界情况,比如k的值于树中节点的总,这时候应该返回一个错误或者特定的值。同时,当k为0或者负时,也需要处理。 举个例子,假设有一个二叉搜索树如下: 4 / \ 2 6 / \ / \ 1 3 5 7 按照逆中序遍历的顺序应该是7、6、5、4、3、2、1。如果k=3,那么第3的元素是5。这时候,遍历到7时计1,6计2,5计3,这时候返回5即可。 接下来,我需要考虑如何用C语言实现这个逻辑。首先定义树节点的结构体: struct TreeNode { int val; struct TreeNode *left; struct TreeNode *right; }; 然后,编写一个递归辅助函数,用来进行逆中序遍历。这个函数需要传入当前节点、k的值、计器指针以及结果指针。或者,可以使用静态变量来保存计和结果,这样函数会更简单,但需要注意静态变量的初始化问题。 可能的结构是: void reverseInorder(struct TreeNode* root, int k, int* count, int* result) { if (root == NULL || *count >= k) { return; } reverseInorder(root->right, k, count, result); if (*count < k) { (*count)++; if (*count == k) { *result = root->val; return; } reverseInorder(root->left, k, count, result); } } 然后,主函数可以调用这个辅助函数: int kthLargest(struct TreeNode* root, int k) { int count = 0; int result = -1; reverseInorder(root, k, &count, &result); return result; } 这样,每次调用kthLargest时,count和result都会被正确初始化为0和-1。递归函数会先遍历右子树,然后处理当前节点,增加count,当count等于k时,记录结果并返回。之后继续遍历左子树,但此时如果count已经于等于k,就会直接返回,不再继续处理。 需要注意的是,当k的值超过树的总节点时,返回的result可能还是初始化的-1,这时候可能需要处理错误情况,或者确保调用时k是有效的。例如,可以先计算树的总节点,然后判断k是否在1到总节点之间。但这一步可能会影响效率,特别是在树很的情况下。如果题目假设k是有效的,那么可能不需要处理。 另外,这种方法的时间复杂度是O(k),因为最多访问k个节点。而空间复杂度是O(h),h是树的高度,即递归栈的深度。 测试一下例子中的情况,当k=3时,应该返回5。那在遍历时,先访问右子树到7,此时count是0。然后返回上一层,处理节点6,count变为1。接着处理右子树为NULL,回到6,继续处理左子树5。访问5的右子树为NULL,处理节点5时count增加到2,然后检查是否等于k=3?这时候count是2,不等于,所以继续处理左子树,还是NULL。返回到节点5的处理完成,count还是2。然后回到节点6的处理,此时count是2,继续处理左子树5的处理已经完成。这时返回到节点4的右子树处理完毕,处理节点4时count增加到3,这时候等于k=3,记录result为4的值?不对,可能哪里出错了。 哦,这里可能我的例子中的遍历顺序有问题。让我再仔细想一遍。原树的结构: 根是4,右子树是6,左子树是2。逆中序遍历的顺序是右-根-左。所以从根开始,先访问右子树6,然后访问6的右子树7。处理7的时候,count增加到1,然后返回处理6,count增加到2,这时候还没到k=3,所以处理6的左子树5。处理5的时候,访问5的右子树为NULL,处理5节点,count增加到3,此时等于k=3,返回结果5。这样就正确了。所以代码应该能够正确捕获这个情况。 另一个测试用例,如果k=1,最的节点是7,代码应该正确返回。而k=7的话,最小节点是1。 那可能这个逻辑是正确的。现在考虑代码是否正确。 在辅助函数reverseInorder中,首先递归调用右子树。当右子树处理完后,处理当前节点:增加count,并检查是否等于k。如果等于,则设置结果。否则,继续处理左子树。这样可以保证处理顺序是右-根-左,即从到小。 所以代码逻辑是对的。 另外,需要考虑如果树中有重复元素的情况,但根据BST的定义,节点值应该是唯一的,左子树小于根,右子树于根。所以假设输入是合法的BST,不会有重复的值。 综上,这个算法是正确的,可以在O(k)时间和O(h)空间内解决问题。</think>要使用C语言实现寻找二叉搜索树(BST)的第k元素,可以遵循以下步骤: ### 一、问题分析 1. **BST特性**:左子树的所有节点值均小于根节点,右子树的所有节点值均于根节点。 2. **逆中序遍历**:中序遍历BST会得到递增序列,而逆中序遍历(右-根-左)会得到递减序列。通过逆中序遍历,第k个访问的节点即为第k的元素。 ### 二、实现思路 1. **递归逆中序遍历**:从右子树开始遍历,依次访问右节点、根节点、左节点。 2. **计与终止条件**:设置计器记录已访问的节点,当计器等于k时终止遍历。 ### 三、代码实现 ```c #include <stdio.h> #include <stdlib.h> // 定义二叉树节点结构 struct TreeNode { int val; struct TreeNode *left; struct TreeNode *right; }; // 辅助函数:逆中序遍历 void reverseInorder(struct TreeNode* root, int k, int* count, int* result) { if (root == NULL || *count >= k) { return; } // 遍历右子树 reverseInorder(root->right, k, count, result); if (*count < k) { (*count)++; if (*count == k) { *result = root->val; return; } // 遍历左子树 reverseInorder(root->left, k, count, result); } } // 主函数寻找第k元素 int kthLargest(struct TreeNode* root, int k) { int count = 0; // 计器 int result = -1; // 存储结果 reverseInorder(root, k, &count, &result); return result; } // 测试代码 int main() { // 构建示例BST: // 4 // / \ // 2 6 // / \ / \ // 1 3 5 7 struct TreeNode n1 = {1, NULL, NULL}; struct TreeNode n3 = {3, NULL, NULL}; struct TreeNode n2 = {2, &n1, &n3}; struct TreeNode n5 = {5, NULL, NULL}; struct TreeNode n7 = {7, NULL, NULL}; struct TreeNode n6 = {6, &n5, &n7}; struct TreeNode root = {4, &n2, &n6}; int k = 3; printf("第%d元素是: %d\n", k, kthLargest(&root, k)); // 输出5 return 0; } ``` ### 四、关键点解析 1. **逆中序遍历顺序**: - 优先遍历右子树,再访问根节点,最后遍历左子树。 - 每访问一个节点时,计器加1,当计器等于k时立即返回结果。 2. **时间复杂度**:$O(k)$,最多遍历k个节点。 3. **空间复杂度**:$O(h)$,h为树的高度(递归栈深度)。 ### 五、边界情况处理 - **k超过节点总**:返回初始值-1(需根据需调整错误处理逻辑)。 - **空树或k=0**:直接返回错误值。 ### 六、总结 通过逆中序遍历,结合计器提前终止遍历,可以高效找到BST的第k元素。此方法充分利用了BST的特性,避免了遍历整个树的开销。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值