基础算法设计-神秘的“树”(一)

2018-06-12  本文已影响0人  芥末芋头

前言

时隔多日又要跑出来作妖了......现在想想当初要是早点打造自己的“品牌”该多好,自从进入了大三,就会要收拾各种各样的事情,至少没有大一大二这么咸鱼了,因此要写这种技术文章都没有很连续,很固定的。就像我的React Native入门指导还没写完(虽然比不过大佬的技术,但是自己遇到的坑也是有一定的价值滴)。
咳,言归正传,这次直接跳过了中间各种各样的算法,打算先发一发两个有关于树,并且挺实用的算法。主要因为正好自己结课做的也是这个。
注:题目来自我们亲爱老班的OJ,若觉有不妥之地请务必联系我,我可以立马收起来......

线段树

例题:

题目描述

给定N个整数代表某天的成交量,现在要处理M个查询。查询分为以下两类:1、找出第i天至第j天的最高成交量;2、统计第i天至第j天的总成交量。(N和M的值比较大)

输入

输出

样例输入

6 4
2 4 3 1 5 2
1 1 3
2 1 3
1 3 6
2 3 6

样例输出

4 9 5 11

来源

[计科老班]

这里说到了N和M比较大,所以很明显最容易想到的方法:直接扫几遍查询出结果肯定是不行的。
这里我们采用这种数据结构,将这个每日成交量构建起来。树的叶子部分,可以想象是每天的成交量本身,节点放置的变量maxsum表示该节点最大值和总和,在叶子上也表现为自己本身。
接着通过递归的方式,将两个节点连接到上一个节点中,构成二叉树,此时这个节点的max值为刚刚两个叶子节点的最大值,sum为刚刚两个叶子节点的总和。

看建树代码

int[] Days;
//create tree for search
    private Node CreateLineTree(int L,int R){
        Node current = new Node();
        current.L = L;current.R = R;
        if(L==R){
            current.max = current.sum = Days[L];
            return current;
        }
        int mid = (L+R)/2;
        current.Left = CreateLineTree(L,mid);
        current.Right = CreateLineTree(mid+1,R);
        current.max = Math.max(current.Left.max, current.Right.max);
        current.sum = current.Left.sum + current.Right.sum;
        
        return current;
    }

L和R表示左边的值和右边的值,在该树中组成一个区间,往后查询时就可以根据这个区间进行筛选。

节点结构

class Node{
        int R,L;
        int max,sum;
        Node Left,Right;
    }

查询的代码

//search the sum between L and R
    private int DisplaySum(Node root,int L,int R){
        if(root.L==L&&root.R==R)return root.sum;
        
        if(R<=root.Left.R)return DisplaySum(root.Left,L,R);
        else if(L>=root.Right.L)return DisplaySum(root.Right,L,R);
        else return (DisplaySum(root.Left,L,root.Left.R)+DisplaySum(root.Right,root.Right.L,R));
    }
    
//search the max between L and R
    private int DisplayMax(Node root,int L,int R){
        if(root.L==L&&root.R==R)return root.max;
        
        if(R<=root.Left.R)return DisplayMax(root.Left,L,R);
        else if(L>=root.Right.L)return DisplayMax(root.Right,L,R);
        else return Math.max(DisplayMax(root.Left,L,root.Left.R), DisplayMax(root.Right,root.Right.L,R));
    }

查询处也可以通过求中位数查询,可能大部分人一开始想到的也是这个(包括我),但其实可以直接利用树中已经存在的值进行判断。
核心的地方就在构建这个线段树,大大的缩短了查询的时间,查询的实现方式应该有各种各样的。

后缀树(字典树)

例题:

题目描述

输入一定量的单词,查询某个单词是否是这些输入的单词中的结尾字母(单词)

输入

开头输入数字,后输入相应个数的单词,直到输入0时才结束此程序。

输出

样例输入

3
ella
Arcella
la
6
ella
Cinderella
abc
bc
c
dbc
0

样例输出

3 6

字典树网上应该也有挺多讲解的,其中还涉及到减少树的高度,提高查询效率的问题,我这里没有涉及,只是单纯的将这种后缀树的实现与查询演示了一遍,感兴趣的可以再去百度一下。
这里比较核心的思想是,将你拿到的单个字符串(单词)反转,也就是从后往前的插入树中,这样子的话我们就可以不用关心这些单词的“传入是否有先后”这个问题了。
之后还需要注意的是,因为是将一个字符串(单词)拆成一个个字母,再将它们放到树中作为一个节点的,需要标记这个字母是不是这个字符串(单词)的结束字母。

先看节点结构

class Node extends HashMap<Character,Node>{ //extend to HashMap so that you new it,you new a HashMap as well.
        private static final long serialVersionUID = 1L;    
        boolean isEnd;  //false,when this character is the end of this string,turn it true.
        int count;  //up one when the character go through it.
    }

这里要先放出节点的构造,因为这颗树并不一定是二叉树,节点不确定,而且我们又需要确定下一个节点的位置,所以我用了HashMap,并且是直接继承的方式,这样子在new这个节点的时候其实就相当于new了一个HashMap。(就是懒得写那句很长的语句)
count这个值,应该是每次经过了存在的节点就进行++操作。

看下核心的构建树代码

//create the tree and return the root of tree.
    private Node createTree(Node root,int position){
        char thisChar = one.charAt(position);
        if(root.containsKey(thisChar)){ //you have it
            root.get(thisChar).count++;
            if(position==0){
                root.get(thisChar).isEnd = true;
                return root;
            }
            else createTree(root.get(thisChar),position-1); //not the end,so move to the next node.
        }else{  //you do not have it
            if(position==0){
                root.put(thisChar, new Node());
                root.get(thisChar).isEnd = true;
                return root;
            }else root.put(thisChar, createTree(new Node(),position-1)); //not the end,create a new node.
        }
        
        return root;
    }

需要注意这里要传入一个根节点,从而使得单次操作中的单词都落到了这颗“”上,还需要一个位置标记,当这个标记为时就应该结束(return)并将标记设置为true,因为这里我是没有采用将字符串反转,而是直接从该字符串(单词)尾部开始插入。
将插入节点划分成两种情况:这棵树拥有这个节点和这棵树没有这个节点。这样子的话应该会好理解一些。
因为我这个节点直接就是HashMap,所以你看代码的时候可以会有点绕,你可以先用纸笔画一下理清楚关系先,我当时写的时候写到一半突然就迷茫了hhhh,我一开始也没有适应来着,老想着是在当前节点的count进行++操作,其实应该去到下一层
而且我这样做的话,不需要考虑“第一个节点不放字母”这个问题。

加和代码

//to result
    private int result(Node root){  //BFS
        int sum = 0;
        Queue<Node> list = new LinkedList<Node>();
        list.add(root);
        while(!list.isEmpty()){
            Node current = list.poll(); //out off this queue.
            for(Entry<Character,Node> entry:current.entrySet()) //foreach 
                list.add(entry.getValue());
            if(current.isEnd)sum+=current.count;
        }
        return sum;
    }

请不要在意我那个蹩脚的英语,自己懂系列。
这里要计算的是单个字符串作为别的字符串结尾单词(字符串)的次数,计算的时候要注意,当且仅当这个标记为true时才取出其中的count进行加和运算。
这里我用的是BFS也就是宽度优先搜索算法,不清楚是不是有更简洁的方式,如果有大佬路过的话可以小小提示我,或者指点我一波。(我就老觉得建了树又再从根开始查询,怪怪的,也没有太多的算法经验)

虽然都是老师教的,或者是网上指点才做出来的,自己也没有想出像这样让人惊叹的算法思路。不过有一点可以肯定的是,思想,还是很重要的东西。

结语

咳咳,对于你大胆的想法,我有一个不成熟的建议。
开玩笑。上面就是一个简单的例题演示,如有什么意见/建议,欢迎提出来。芥末是前端方向的,不过这些基础的东西,自己还是要懂得。这些文章更多的是在做自己的笔记同时分享出去,希望能帮到一些同学。
GitHub:https://github.com/Eugenehyj
另有篇关于RN的一些新手心得(待更新)
——“小白”的前端之路

上一篇下一篇

猜你喜欢

热点阅读