数据流中数字的秩

题目描述

Imagine you are reading in a stream of integers. Periodically, you wish to be able to look up the rank of a number x (the number of values less than or equal to x). lmplement the data structures and algorithms to support these operations. That is, implement the method track (int x), which is called when each number is generated, and the method getRankOfNumber(int x), which returns the number of values less than or equal to x.

面试题 10.10. Rank from Stream LCCI 中等之困难

题解

暴力普通数组

思路

假设整数流中的整数在0~50000之间,那么我们可以预先建立一个大小为50001的int[]数组,来记录每个整数出现的次数,查询比n小的个数时,只需从0遍历到n即可。

代码

class StreamRank {
    int[] nums;
    public StreamRank() {
        nums = new int[50001];
    }
    public void track(int x) {
        nums[x]++;
    }
    public int getRankOfNumber(int x) {
        int ans = 0;
        for (int i = 0;i <= x;i++){
            ans += nums[i];
        }
        return ans;
    }
}

时间复杂度分析

添加元素只需自增,为\(O(1)\),查询时需要遍历所有比n小的数,时间复杂度为\(O(n)\)。若查询次数为m,插入次数为n,则时间复杂度为\(O(n+mn)\)


暴力前缀和数组

思路

假设同一,将普通数组改为前缀和数组。

代码

class StreamRank {
    int[] nums;
    public StreamRank() {
        nums = new int[50001];
    }
    public void track(int x) {
        for(int i = x;i <= 50000;i++){
            nums[i]++;
        }
    }
    public int getRankOfNumber(int x) {
        return nums[x];
    }
}

时间复杂度分析

和解法一正相反,前缀和数组插入的时间复杂度是\(O(n)\),查询的时间复杂度为\(O(1)\)。若查询次数为m,插入次数为n,则时间复杂度为\(O(n^2+m)\)


二叉搜索树

解法一:每个结点额外存储左子树中小于本结点的结点数目以及自身数目之和

class StreamRank {
    BSTree bsTree;
    public StreamRank() {
        bsTree = new BSTree();
    }
    public void track(int x) {
        bsTree.add(x);
    }
    public int getRankOfNumber(int x) {
        return bsTree.query(x);
    }
}
class BSTree{
    TreeNode root;
    public BSTree(){

    }
    public void add(int x){
        if(root == null){
            root = new TreeNode(x);
        }
        else{
            TreeNode p = root;
            while(true){
                if(x == p.val){//树中已有该值,直接把数量加1
                    p.num++;
                    break;
                }
                else if(x < p.val){
                    if(p.left == null){
                        p.left = new TreeNode(x);
                        p.num++;//若是左子树,则当前结点num加1
                        break;
                    }
                    p.num++;//若是左子树,则当前结点num加1
                    p = p.left;
                }
                else{
                    if(p.right == null){
                        p.right = new TreeNode(x);
                        break;
                    }
                    p = p.right;
                }
            }
        }
    }
    public int query(int x){
        return queryFunc(root, x);
    }
    private int queryFunc(TreeNode root, int x){
        if(root == null){
            return 0;
        }
        if(x == root.val){//恰好命中,返回该命中结点的num域
            return root.num;
        }
        else if(x < root.val){//递归在左子树中查找
            return queryFunc(root.left, x);
        }
        else{//先加上根节点的num域,再递归在右子树中查找
            return root.num + queryFunc(root.right, x);
        }
    }
}
class TreeNode{
    int val;
    int num;//存储左子树中小于本结点的个数与本结点数目之和
    TreeNode left;
    TreeNode right;
    public TreeNode(int val){
        this.val = val;
        this.num = 1;
    }
}

解法二:每个结点额外存储自身数目以及子结点总数两个域

class StreamRank {
    BSTree bsTree;
    public StreamRank() {
        bsTree = new BSTree();
    }
    public void track(int x) {
        bsTree.add(x);
    }
    public int getRankOfNumber(int x) {
        return bsTree.query(x);
    }
}
class BSTree{
    TreeNode root;
    public BSTree(){

    }
    public void add(int x){
        if(root == null){
            root = new TreeNode(x);
        }
        else{
            TreeNode p = root;
            while(true){
                if(x == p.val){//等于当前结点,表明树中已有该值,直接把结点自身数量加1
                    p.selfNum++;
                    break;
                }
                else if(x < p.val){//小于当前结点
                    if(p.left == null){//若当前结点左子树为空,则新建结点,并将当前结点子结点数目加1
                        p.left = new TreeNode(x);
                        p.childNum++;
                        break;
                    }
                    p.childNum++;//若当前结点左子树非空,继续向左寻找,并将当前结点子结点数目加1
                    p = p.left;
                }
                else{//大于当前节点
                    if(p.right == null){//若当前结点右子树为空,则新建结点,并将当前结点子节点数目加1
                        p.right = new TreeNode(x);
                        p.childNum++;
                        break;
                    }
                    p.childNum++;//若当前结点右子树非空,继续向右寻找,并将当前结点子节点数目加1
                    p = p.right;
                }
            }
        }
    }
    public int query(int x){
        return queryFunc(root, x);
    }
    private int queryFunc(TreeNode root, int x){
        if(root == null){
            return 0;
        }
        if(x == root.val){
            //查询结点等于当前结点,那么小于等于查询结点的数目=该结点的selfNum+左子树的结点数(即左孩子的selfNum+左孩子的子结点数目)
            return root.selfNum + (root.left == null ? 0 : root.left.childNum + root.left.selfNum);
        }
        else if(x < root.val){
            //查询结点小于当前结点,那么递归地在左子树中查询
            return queryFunc(root.left, x);
        }
        else{
            //查询结点大于当前结点,那么就已经知道,至少根结点和左子树的所有结点都小于查询结点
            //于是先加上这些已知的结点数目,再递归地在右子树中查询
            return root.selfNum + (root.left == null ? 0 : root.left.childNum + root.left.selfNum) + queryFunc(root.right, x);
        }
    }
}
class TreeNode{
    int val;
    int selfNum;//结点自身数目
    int childNum;//子结点个数
    TreeNode left;
    TreeNode right;
    public TreeNode(int val){
        this.val = val;
        this.childNum = 0;
        this.selfNum = 1;
    }
}

复杂度分析

对于二叉搜索树,平均每次插入和查询为\(O(logn)\)的时间复杂度。n次插入和查询则为\(O(nlogn)\)的时间复杂度。

最坏情况下,二叉搜索树形成一条链,时间复杂度接近\(O(n^2)\)

算法改进

由于特殊情况下二叉查找树的退化,本题可以使用改进的二叉查找自平衡树,如AVL树、红黑树等。

由于AVL树、红黑树的实现代码比较复杂,这里就不再讨论。对于本题,除了树的具体实现方式不同,他们额外添加的结点信息都是相同的。


树状数组

思路

限制数据流中的数字最大为50000、最小为0的情况下,使用树状数组可以在O(logn)的时间内快速更新和查询。

代码

class StreamRank {
    FenwickTree fenwickTree;
    public StreamRank() {
        fenwickTree = new FenwickTree(50001);
    }
    
    public void track(int x) {
        fenwickTree.update(x, 1);
    }
    
    public int getRankOfNumber(int x) {
        return fenwickTree.query(x);
    }
}
class FenwickTree{
    int[] tree;
    int len;
    public FenwickTree(int n){
        this.tree = new int[n + 1];
        this.len = n;
    }
    public void update(int i, int dest){
        i++;//由于0的存在,向右平移一位
        while(i <= len){
            tree[i] += dest;
            i += lowbit(i);
        }
    }
    public int query(int i){
        i++;//由于0的存在,向右平移一位
        int sum = 0;
        while(i > 0){
            sum += tree[i];
            i -= lowbit(i);
        }
        return sum;
    }
    private int lowbit(int x){
        return x & (-x);
    }
}

复杂度分析:

使用树状数组可以在\(O(logn)\)的时间内快速更新和查询。n次更新和查询的时间复杂度为\(O(nlogn)\)

缺点及改进:

当数据流中的数字过大时,需要开辟较大数组,不满足实际要求。

虽然可以通过映射排名离散化和离线处理的手段,但不符合数据流实时读入和查询的在线要求,当然,如果只是为了得到数据流的结果而不强调实时性,所有查询都可以在最后输出,这种离线处理依然可行。


References:
[1] 数据结构与算法分析:Java语言描述-Mark Allen Weiss
[2] 程序员面试金典-Gayle Laakmann McDowell

posted @ 2021-04-20 22:32  HickeyZhang  阅读(175)  评论(0编辑  收藏  举报