TOPN问题 十万级数据取前K大的数据解法

TOPN问题 十万级数据取前K大的数据解法

题目描述

在10万个数字中寻找前100大的数字,输出最大的前 d个数到终端,并输出比较的次数

解法要求

1.不能使用堆排序,检验时请自行生成1~10万的随机数。要求在20万次比较中得到答案
2.且需要使用类似足球晋级的二叉树排序
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3.不允许碎片化申请空间,需要一次性申请一个大数组作为存储,尽量在满足上述要求的情况下减少申请空间

算法思路

可以采用分治的思想构建二叉树,从左往右找到前K大个数

1.快速排序
首先生成n个随机整数,再对这n个整数进行快速排序,得到排序后的新数组。该快速排序采用了分治策略,对数组进行递归分解和合并处理。

2.构建二叉树
然后,用上述排好序的n个数字构建一颗二叉树。该二叉树是一颗满二叉树,每一个节点只存储对应区间内的最大数字。对于每一个节点,需要比较左右儿子节点中的较大值,并将该较大值作为该节点的值。递归地执行这一过程,直到树根节点处仍然只有一个数字。

3.从树根节点一次遍历,找出前k大的数字
在构建好二叉树之后,从根节点开始遍历,不断选择左右节点中的最大值,直到找出前k大的数字。在递归搜索的过程中,如果遇到数字数量少于k的区间,直接输出其中所有的数字即可。最后统计比较次数并输出结果。

基于go语言的代码实现

package main // 定义主程序包
import (     // 导入需要引用的包
	"fmt"
	"math/rand"
	"time"
)

const ( // 定义常量
	n = 100000 // 数字总数
	d = 100    // 前d大的数字
)

// 快速排序算法
func quickSort(nums []int) { // 定义快速排序函数
	if len(nums) <= 1 { // 如果只有一个数字,直接返回
		return
	}
	pivot := nums[len(nums)/2] // 获取中间的“轴点”
	i, j := 0, len(nums)-1     // 定义左右指针
	for i <= j {               // 如果左指针小于等于右指针
		for nums[i] > pivot { // 找到左边第一个小于等于pivot的数字
			i++
		}
		for nums[j] < pivot { // 找到右边第一个大于等于pivot的数字
			j--
		}
		if i <= j { // 如果左指针仍然小于等于右指针,交换左右指针指向的数字
			nums[i], nums[j] = nums[j], nums[i]
			i++
			j--
		}
	}
	if j+1 >= d { // 如果右指针的位置比d小
		quickSort(nums[:j+1]) // 递归地对左半部分进行排序
	}
	if i-1 < n-d { // 如果左指针的位置比n-d大
		quickSort(nums[i:]) // 递归地对右半部分进行排序
	}
}

var (
	nums       [n + 1]int             // 存储数字的数组
	tree       [4*n + 1]int           // 存储二叉树的数组
	cmpCnt     int                    // 比较次数计数器
	winnerFunc = func(a, b int) int { // 使用函数变量封装比较操作
		cmpCnt++               // 计数器自增
		if nums[a] > nums[b] { // 比较a和b所表示的数字
			return a
		} else {
			return b
		}
	}
)

// 建立二叉树
func buildTree(node, l, r int) { // 定义建立二叉树的函数
	if l == r { // 如果只剩下一个数字
		tree[node] = l // 将该数字作为当前节点
		return
	}
	mid := (l + r) / 2                                    // 获取区间的中间位置
	buildTree(node*2, l, mid)                             // 递归地建立左子树
	buildTree(node*2+1, mid+1, r)                         // 递归地建立右子树
	tree[node] = winnerFunc(tree[node*2], tree[node*2+1]) // 将这两个子节点中的胜者作为当前节点
}

// 分治算法,对二叉树的节点进行排序
func divide(node, l, r int) { // 定义分治排序函数,对二叉树的节点进行排序
	// 叶子节点
	if l == r { // 如果只剩下一个数字
		if r <= d { // 如果该数字是前d大的数字之一,则将它输出
			fmt.Print(nums[l], " ")
		}
		return
	}
	// 非叶子节点
	mid := (l + r) / 2                                    // 获取当前区间的中间位置
	divide(node*2, l, mid)                                // 递归处理左子树,得到左子树中前100大的数字
	divide(node*2+1, mid+1, r)                            // 递归处理右子树,得到右子树中前100大的数字
	tree[node] = winnerFunc(tree[node*2], tree[node*2+1]) // 将这两个子节点中的胜者作为当前节点
	// 输出前d大数字
	if l <= 1 && r >= d {
		fmt.Print(nums[tree[node]], " ")
	}
}

func main() {
	rand.Seed(time.Now().UnixNano()) // 随机化种子
	// 生成随机数,并进行去重
	m := make(map[int]bool) // 利用map来去除重复数字
	for i := 1; i <= n; {   // 逐个生成数字
		val := rand.Intn(1000000) // 生成一个从0到1000000的随机数
		if !m[val] {              // 如果这个数字没有出现过
			nums[i] = val // 将这个数字存入数组中
			m[val] = true // 表示这个数字已经出现过了
			i++           // 数组计数器自增
		}
	}
	//fmt.Println("Original array:", nums[1:]) // 打印原始数组
	// 排序
	quickSort(nums[1:])                    // 对nums[1:]中的n个数字进行排序
	fmt.Println("Sorted array:", nums[1:]) // 打印排序后的数字
	// 构建二叉树并排序
	buildTree(1, 1, n) // 建立二叉树
	divide(1, 1, n)    // 对二叉树进行分治排序

	// 输出比较次数
	fmt.Println("\nComparison Count:", cmpCnt)
}

测试结果

10W数据找前100大数据

在这里插入图片描述

7个数据找前3大数据

在这里插入图片描述

时间空间复杂度分析

上述代码中,首先使用快速排序算法对n个数字进行排序,其中快速排序的时间复杂度为O(nlog(n))。然后,使用一个大小为4n+1的数组来存储二叉树,其中扣除输入的n和d后为O(n)的空间复杂度。因此,总的空间复杂度为O(n)。之后,我们使用分治算法在O(log(n))的时间复杂度内对二叉树中的节点进行排序,得到前100大的数字,并输出到终端。在分治排序的过程中,每次需要进行两次比较,因此总的比较次数为2nlog(n)次,时间复杂度为O(n*log(n))。

综上所述,该程序的时间复杂度为O(nlog(n)),空间复杂度为O(n),比较次数为2n*log(n),程序可以在20万次比较的限制内找到前100大的数字。这个程序可以应对多种数据规模的应用场景,并且不需要额外的空间申请,是一个高效,稳定的算法。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值