从零开始学习——树(一)

文章整理自互联网https://www.cnblogs.com/huangxincheng/archive/2012/08/04/2623455.html

还有几篇的链接找不到了 当时做笔记时没存

 

树的定义(超简述 简单理解)

1.什么是树?

是一种数据结构,可以用来表示层次关系,因表示的样子很像一颗倒立的树而得名。

在数据结构中的特点,是一对多(链表是一对一,图是多对多)

 

 

最上面;树根

中间:树枝

最下:树叶

2.树的定义。

如图:

 

深度就是指从根开始的距离(即从根开始计算,假如根为第0个结点,那么连接他和根的线段中,他是第几个结点,深度就是几);

而高度就是指,一个结点,和最底层距离有多远,假如最多是5层,这个结点和根的距离只有1(即位于第4层),那么他的高度就是4。

 

二叉树

一、基本性质:

1) 在二叉树中,第i层的结点总数不超过2^(i-1);

2) 深度为h的二叉树最多有2^h-1个结点(h>=1),最少有h个结点;

3) 对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1;

4) 具有n个结点的完全二叉树的深度为int(log2n)+1

5)给定N个节点,能构成h(N)种不同的二叉树。h(N)为卡特兰数的第N项。h(n)=C(n,2*n)/(n+1)。

二、什么是完全二叉树?

若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层有叶子结点,并且叶子结点都是从左到右依次排布。

判断:叶子节点为左节点时是完全二叉树,叶子节点为右节点时,不连续紧密排列,非完全二叉树

 

三、什么是满二叉树? 

除了叶结点外每一个结点都有左右子结点且叶子结点都处在最底层的二叉树。

每个非叶子节点都有两个叶子节点

四、存储方式

存储的方式和图一样,有链表和数组两种,用数组存访问速度快,但插入、删除节点操作就比较费时了。实际中更多的是用链来表示二叉树的。

五、遍历方法

分为前序遍历,中序遍历和后序遍历,和按层次遍历。前三种方法可用递归实现,比较简单,不用递归的可以自己模拟栈操作来实现非递归的遍历方法。按层遍历也比较简单,就是自己实现一个队列就可以了,也可以用数组来模拟。需要注意的是后序遍历的非递归算法,需要一个标识来标识某个节点的状态,具体的就是当一个节点左孩子被遍历的时候为一个状态,当左右两个节点都被遍历的时候又是一个状态,只有一个节点的左右孩子几点都被遍历,才可以输出这个节点的值,这个和后序遍历的定义差不多。

接下来看是如何遍历的。

前序遍历

 

遍历的结果是:1、2、4、5、7、8、3、6。从根结点分两部分,先把左边的遍历完,都是从左到右的。

 

中序遍历

 

结果是:4,2,7,8,5,1,3,6。

后序遍历

 

 

结果是:4、8、7、5、2、6、3、1

层次遍历

 

结果是:1、2、3、4、5、6、7、8

 

六、二叉树与树的区别?

1)树中结点的最大度数没有限制,而二叉树结点的最大度数为2;

 2)树的结点无左、右之分,而二叉树的结点有左、右之分。

 3)树是一种简单的非线性结构,所有元素之间具有明显的层次特性。

 4)在树结构中,每一个结点只有一个前件,称为父结点,没有前件的结点只有一个,称为树的根结点,简称树的根。每一个结点可以有多个后件,称为该结点的子结点。没有后件的结点称为叶子结点。

5)二叉树的特点:非空二叉树只有一个根结点,每一个结点最多有两棵子树,且分别称为该结点的左子树与右子树。

 

 

二叉查找树

(1)若左子树不空,则左子树上所有结点的值均小于或等于它的根结点的值;

(2)若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;

(3)左、右子树也分别为二叉排序树;

 

一:场景:

1:现状

    前几天我的一个大学同学负责的网站出现了严重的性能瓶颈,由于业务是写入和读取都是密集型,如果做缓存,时间间隔也只能在30s左

右,否则就会引起客户纠纷,所以同学也就没有做缓存,通过测试发现慢就慢在数据读取上面,总共需要10s,天啊...原来首页的加载关联

到了4张表,而且表数据中最多的在10w条以上,可以想象4张巨大表的关联,然后就是排序+范围查找等等相关的条件,让同学抓狂。

 

2:我个人的提供解决方案

 ① 读取问题

    既然不能做缓存,那没办法,我们需要自己维护一套”内存数据库“,数据如何组织就靠我们的算法功底了,比如哈希适合等于性的查找,

树结构适合”范围查找“,lucene适合字符串的查找,我们在添加和更新的时候同时维护自己的内存数据库,最终杜绝表关联,老同学,还

是先应急,把常用的表灌倒内存,如果真想项目好的话,改架构吧...

② 添加问题

   或许你的Add操作还没有达到瓶颈这一步,如果真的达到了那就看情况来进行”表切分“,”数据库切分“吧,让用户的Add或者Update

操作分流,虽然做起来很复杂,但是没办法,总比用户纠纷强吧,可对...

 

二:二叉查找树

    正式切入主题,从上面的说明我们知道了二叉树非常适合于范围查找,关于树的基本定义,这里我就默认大家都知道,我就直接从

查找树说起了。

1:定义

   查找树的定义非常简单,一句话就是左孩子比父节点小,右孩子比父节点大,还有一个特性就是”中序遍历“可以让结点有序。

 

2:树节点

为了具有通用性,我们定义成泛型模板,在每个结点中增加一个”数据附加域”。

/// <summary>
    /// 二叉树节点
    /// </summary>
    /// <typeparam name="K"></typeparam>
    /// <typeparam name="V"></typeparam>
    public class BinaryNode<K, V>
    {
        /// <summary>
        /// 节点元素
        /// </summary>
        public K key;

        /// <summary>
        /// 节点中的附加值
        /// </summary>
        public HashSet<V> attach = new HashSet<V>();

        /// <summary>
        /// 左节点
        /// </summary>
        public BinaryNode<K, V> left;

        /// <summary>
        /// 右节点
        /// </summary>
        public BinaryNode<K, V> right;

        public BinaryNode() { }

        public BinaryNode(K key, V value, BinaryNode<K, V> left, BinaryNode<K, V> right)
        {
            //KV键值对
            this.key = key;
            this.attach.Add(value);

            this.left = left;
            this.right = right;
        }
    }

3:添加

   根据查找树的性质我们可以很简单的写出Add的代码,一个一个的比呗,最终形成的效果图如下

这里存在一个“重复节点”的问题,比如说我在最后的树中再插入一个元素为15的结点,那么此时该怎么办,一般情况下,我们最好

不要在树中再追加一个重复结点,而是在“重复节点"的附加域中进行”+1“操作。

 

#region 添加操作
        /// <summary>
        /// 添加操作
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void Add(K key, V value)
        {
            node = Add(key, value, node);
        }
        #endregion

        #region 添加操作
        /// <summary>
        /// 添加操作
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="tree"></param>
        /// <returns></returns>
        public BinaryNode<K, V> Add(K key, V value, BinaryNode<K, V> tree)
        {
            if (tree == null)
                tree = new BinaryNode<K, V>(key, value, null, null);

            //左子树
            if (key.CompareTo(tree.key) < 0)
                tree.left = Add(key, value, tree.left);

            //右子树
            if (key.CompareTo(tree.key) > 0)
                tree.right = Add(key, value, tree.right);

            //将value追加到附加值中(也可对应重复元素)
            if (key.CompareTo(tree.key) == 0)
                tree.attach.Add(value);

            return tree;
        }
        #endregion

4:范围查找

    这个才是我们使用二叉树的最终目的,既然是范围查找,我们就知道了一个”min“和”max“,其实实现起来也很简单,

第一步:我们要在树中找到min元素,当然min元素可能不存在,但是我们可以找到min的上界,耗费时间为O(logn)。

第二步:从min开始我们中序遍历寻找max的下界。耗费时间为m。m也就是匹配到的个数。

 

最后时间复杂度为M+logN,要知道普通的查找需要O(N)的时间,比如在21亿的数据规模下,匹配的元素可能有30个,那么最后

的结果也就是秒杀和几个小时甚至几天的巨大差异,后面我会做实验说明。

 

#region 树的指定范围查找
        /// <summary>
        /// 树的指定范围查找
        /// </summary>
        /// <param name="min"></param>
        /// <param name="max"></param>
        /// <returns></returns>
        public HashSet<V> SearchRange(K min, K max)
        {
            HashSet<V> hashSet = new HashSet<V>();

            hashSet = SearchRange(min, max, hashSet, node);

            return hashSet;
        }
        #endregion

        #region 树的指定范围查找
        /// <summary>
        /// 树的指定范围查找
        /// </summary>
        /// <param name="range1"></param>
        /// <param name="range2"></param>
        /// <param name="tree"></param>
        /// <returns></returns>
        public HashSet<V> SearchRange(K min, K max, HashSet<V> hashSet, BinaryNode<K, V> tree)
        {
            if (tree == null)
                return hashSet;

            //遍历左子树(寻找下界)
            if (min.CompareTo(tree.key) < 0)
                SearchRange(min, max, hashSet, tree.left);

            //当前节点是否在选定范围内
            if (min.CompareTo(tree.key) <= 0 && max.CompareTo(tree.key) >= 0)
            {
                //等于这种情况
                foreach (var item in tree.attach)
                    hashSet.Add(item);
            }

            //遍历右子树(两种情况:①:找min的下限 ②:必须在Max范围之内)
            if (min.CompareTo(tree.key) > 0 || max.CompareTo(tree.key) > 0)
                SearchRange(min, max, hashSet, tree.right);

            return hashSet;
        }
        #endregion

5:删除

   对于树来说,删除是最复杂的,主要考虑两种情况。

<1>单孩子的情况

     这个比较简单,如果删除的节点有左孩子那就把左孩子顶上去,如果有右孩子就把右孩子顶上去,然后打完收工。

<2>左右都有孩子的情况。

     首先可以这么想象,如果我们要删除一个数组的元素,那么我们在删除后会将其后面的一个元素顶到被删除的位置,如图

       

 

那么二叉树操作同样也是一样,我们根据”中序遍历“找到要删除结点的后一个结点,然后顶上去就行了,原理跟"数组”一样一样的。

同样这里也有一个注意的地方,在Add操作时,我们将重复元素的值追加到了“附加域”,那么在删除的时候,就可以先判断是

不是要“-1”操作而不是真正的删除节点,其实这里也就是“懒删除”,很有意思。

#region 删除当前树中的节点
        /// <summary>
        /// 删除当前树中的节点
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public void Remove(K key, V value)
        {
            node = Remove(key, value, node);
        }
        #endregion

        #region 删除当前树中的节点
        /// <summary>
        /// 删除当前树中的节点
        /// </summary>
        /// <param name="key"></param>
        /// <param name="tree"></param>
        /// <returns></returns>
        public BinaryNode<K, V> Remove(K key, V value, BinaryNode<K, V> tree)
        {
            if (tree == null)
                return null;

            //左子树
            if (key.CompareTo(tree.key) < 0)
                tree.left = Remove(key, value, tree.left);

            //右子树
            if (key.CompareTo(tree.key) > 0)
                tree.right = Remove(key, value, tree.right);

            /*相等的情况*/
            if (key.CompareTo(tree.key) == 0)
            {
                //判断里面的HashSet是否有多值
                if (tree.attach.Count > 1)
                {
                    //实现惰性删除
                    tree.attach.Remove(value);
                }
                else
                {
                    //有两个孩子的情况
                    if (tree.left != null && tree.right != null)
                    {
                        //根据二叉树的中顺遍历,需要找到”有子树“的最小节点
                        tree.key = FindMin(tree.right).key;

                        //删除右子树的指定元素
                        tree.right = Remove(key, value, tree.right);
                    }
                    else
                    {
                        //单个孩子的情况
                        tree = tree.left == null ? tree.right : tree.left;
                    }
                }
            }

            return tree;
        }
        #endregion

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值