对于整数数组类的算法的终极解决方案

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对于整数数组类的算法的终极解决方案相关的知识,希望对你有一定的参考价值。

首先来思考一个问题,现在有一个数组A = [1,2,3,4,5,4,3,2,1,2,3,4,5,4,3,2,1],数组内有一些元素有重复数据,现在要求你给出对于数组中的每一个元素,在右(左)侧有多少元素和它相等(不包括本身),有多少元素大于它,有多少元素的两倍小于(大于)它,3倍,平方.... 甚至更加一般的,有多少元素 (A[i],A[j])(i<j<A.length) 满足Fun(A[i],A[j])?

最笨的一种方法:

int res = 0;
for(int i=0;i<A.length;i++){
    for(int j=i+1;j<A.length;j++){
        if(Fun(A[i],A[j]) == true) res += 1;
    }
}
return res;

这种方法的时间复杂度是O(n*n/2),显然是难以让人满意的。

在算法领域,要想降低算法复杂度,就要提高空间复杂度,即所谓的拿空间换时间。如果我们想用空间换时间该怎么换呢?

我们把数组从后向前遍历,每遍历一个元素就把它放到一个仓库里,然后每次求这个元素在仓库里有几个符合条件的值时,直接去仓库里找。只要在仓库里查找和添加、删除足够快(在O(C)时间内完成),我们的时间复杂度就会降低到O(C*n),这显然是可以接受的。

那么有没有这么一种仓库(数据结构),可以实现常数时间内查找、添加、删除元素呢?答案是有的,就是字典树。

我们知道,每一个int都有32位组成。而每一位只有可能是0或者1.这样,只要创建一个深度为32的二叉树就可以描述所有int了。对于每个int的插入、删除、查找的时间都是是32,可见字典树是可以满足我们的需求的。

 

 

将问题发散开去,字典树适用于创建那种集合整体可以被[有限个维度]描述的"仓库",而且对单个元素的增加、修改、查找的时间复杂度就是[维度]。


例如英文文章的词频统计,记录每个单词出现的频率,完全可以用字典树来实现。描述所有单词的一个维度就是单词的内容,即一串英文字符,他们的长度虽然可能各不相同,但是总体上是有限的,即[1,100]。而英文字符也是有限集合,通过ASIIC码可以区分他们。

要想用好字典树,一个很关键的因素就是能不能找到一组有限维度来描述整个集合。

然后贴上一段代码:

/******
 * 利用字典树保存数组中所有的数*2的值,之后从右向左遍历数组,首先将这个数*2的值从树中删除,然后查找树中比
 * 这个数小的元素的个数。
 * 最坏时间复杂度: O(96 * n)
 * 插入、删除、查找还利用了尾递归来加快速度
 **/ 
public class Solution {
    public int reversePairs(int[] nums) {
        int res = 0,lenn = nums.length;//,halfMax = (Integer.MAX_VALUE)/2+10;
        if(lenn<2) return 0;
        
        
        Node head = new Node();
        long temp = nums[lenn-1];
        
        //if(nums[lenn-1] < halfMax) 
        addNum(temp *2,head,63);
        for(int i=lenn-2;i>-1;i--){
            temp = nums[i];
            res += getRes(temp,head,63,0);
            addNum(temp *2,head,63);
        }
        return res;
    }
    private void delNum(long tar,Node he,int bit){
        // 删除树中这个元素,并且更新相关节点的值
        if(bit<0)return ;
        if((tar & (((long)1)<<bit)) == 0){
            if(he.zero.num > 0)he.zero.num -= 1;
            //else return ;
            he = he.zero;
        }else{
            if(he.one.num > 0)he.one.num -= 1;
            //else return ;
            he = he.one;
        }
        delNum(tar,he,bit-1);
    }
    private void addNum(long tar,Node he,int bit){
        // 向树中增加这个元素,并且更新相关节点的值
        if(bit<0)return;
        if((tar & (((long)1)<<bit)) == 0){
            if(he.zero == null) he.zero = new Node();
            else he.zero.num += 1;
            he = he.zero;
        }else{
            if(he.one == null) he.one = new Node();
            else he.one.num += 1;
            he = he.one;
        }
        addNum(tar,he,bit-1);
    }
    private int getRes(long tar,Node he,int bit,int res){
        // 符号位特殊判断
        if(tar>=0){
            if(he.one != null)res += he.one.num;
            he = he.zero;
        }else{
            he = he.one;
        }
        return getSmallerNum(tar,he,bit-1,res);
    }
    private int getSmallerNum(long tar,Node he,int bit,int res){
        // 获取比tar小的所有数的个数,据说是尾递归
        if(bit<0 || he == null) return res;
        if((tar & (((long)1)<<bit)) == 0){
            he = he.zero;
        }else{
            if(he.zero != null)res += he.zero.num;
            he = he.one;
        }
        return getSmallerNum(tar,he,bit-1,res);
    }
    private static class Node{
        public int num;
        public Node zero,one;
        public Node(){
            num = 1;
            zero = null;
            one = null;
        }
    }
}

 

以上是关于对于整数数组类的算法的终极解决方案的主要内容,如果未能解决你的问题,请参考以下文章

冒泡排序法三部曲终极版の最优的冒泡排序算法

如何在未排序数组的情况下找到未排序数组中的第k个最小整数?

THREE.js - 大型int作为Uniform

以下代码片段的算法复杂度

腾讯算法岗一面算法题——计数排序

数的划分终极版--背包法解决各类数的划分