6天通吃树结构—— 第一天 二叉查找树

        一直很想写一个关于树结构的专题,再一个就是很多初级点的码农会认为树结构无用论,其实归根到底还是不清楚树的实际用途。

 

一:场景:

1:现状

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

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

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

 

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

 ① 读取问题

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

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

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

② 添加问题

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

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

 

二:二叉查找树

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

查找树说起了。

1:定义

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

6天通吃树结构—— 第一天 二叉查找树

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的代码,一个一个的比呗,最终形成的效果图如下

6天通吃树结构—— 第一天 二叉查找树

这里存在一个“重复节点”的问题,比如说我在最后的树中再插入一个元素为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>单孩子的情况

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

6天通吃树结构—— 第一天 二叉查找树

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

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

       6天通吃树结构—— 第一天 二叉查找树

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

6天通吃树结构—— 第一天 二叉查找树

同样这里也有一个注意的地方,在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

 

三:测试

   假如现在我们有一张User表,我要查询"2012/7/30 4:30:00"到"2012/7/30 4:40:00"这个时间段登陆的用户,我在txt中生成一个

33w的userid和time的数据,看看在33w的情况下读取效率如何...

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.IO;
using System.Diagnostics;

namespace DataStruct
{
    class Program
    {
        static void Main(string[] args)
        {
            List<long> list = new List<long>();

            Dictionary<DateTime, int> dic = new Dictionary<DateTime, int>();

            BinaryTree<DateTime, int> tree = new BinaryTree<DateTime, int>();

            using (StreamReader sr = new StreamReader(Environment.CurrentDirectory + "//1.txt"))
            {
                var line = string.Empty;

                while (!string.IsNullOrEmpty(line = sr.ReadLine()))
                {
                    var userid = Convert.ToInt32(line.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)[0]);

                    var time = Convert.ToDateTime(line.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)[1]);

                    //防止dic出错,为了进行去重处理
                    if (!dic.ContainsKey(time))
                    {
                        dic.Add(time, userid);

                        tree.Add(time, userid);
                    }
                }
            }

            var min = Convert.ToDateTime("2012/7/30 4:30:00");

            var max = Convert.ToDateTime("2012/7/30 4:40:00");

            var watch = Stopwatch.StartNew();

            var result1 = dic.Keys.Where(i => i >= min && i <= max).Select(i => dic[i]).ToList();

            watch.Stop();

            Console.WriteLine("字典查找耗费时间:{0}ms,获取总数:{1}", watch.ElapsedMilliseconds, result1.Count);

            watch = Stopwatch.StartNew();

            var result2 = tree.SearchRange(min, max);

            watch.Stop();

            Console.WriteLine("二叉树耗费时间:{0}ms,获取总数:{1}", watch.ElapsedMilliseconds, result2.Count);
        }
    }

    #region 二叉树节点
    /// <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;
        }
    }
    #endregion

    public class BinaryTree<K, V> where K : IComparable
    {
        public BinaryNode<K, V> node = null;

        #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

        #region 是否包含指定元素
        /// <summary>
        /// 是否包含指定元素
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public bool Contain(K key)
        {
            return Contain(key, node);
        }
        #endregion

        #region 是否包含指定元素
        /// <summary>
        /// 是否包含指定元素
        /// </summary>
        /// <param name="key"></param>
        /// <param name="tree"></param>
        /// <returns></returns>
        public bool Contain(K key, BinaryNode<K, V> tree)
        {
            if (tree == null)
                return false;
            //左子树
            if (key.CompareTo(tree.key) < 0)
                return Contain(key, tree.left);

            //右子树
            if (key.CompareTo(tree.key) > 0)
                return Contain(key, tree.right);

            return true;
        }
        #endregion

        #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

        #region 找到当前树的最小节点
        /// <summary>
        /// 找到当前树的最小节点
        /// </summary>
        /// <returns></returns>
        public BinaryNode<K, V> FindMin()
        {
            return FindMin(node);
        }
        #endregion

        #region 找到当前树的最小节点
        /// <summary>
        /// 找到当前树的最小节点
        /// </summary>
        /// <param name="tree"></param>
        /// <returns></returns>
        public BinaryNode<K, V> FindMin(BinaryNode<K, V> tree)
        {
            if (tree == null)
                return null;

            if (tree.left == null)
                return tree;

            return FindMin(tree.left);
        }
        #endregion

        #region 找到当前树的最大节点
        /// <summary>
        /// 找到当前树的最大节点
        /// </summary>
        /// <returns></returns>
        public BinaryNode<K, V> FindMax()
        {
            return FindMin(node);
        }
        #endregion

        #region 找到当前树的最大节点
        /// <summary>
        /// 找到当前树的最大节点
        /// </summary>
        /// <param name="tree"></param>
        /// <returns></returns>
        public BinaryNode<K, V> FindMax(BinaryNode<K, V> tree)
        {
            if (tree == null)
                return null;

            if (tree.right == null)
                return tree;

            return FindMax(tree.right);
        }
        #endregion

        #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(tree.key, value, tree.right);
                    }
                    else
                    {
                        //单个孩子的情况
                        tree = tree.left == null ? tree.right : tree.left;
                    }
                }
            }

            return tree;
        }
        #endregion
    }
}

6天通吃树结构—— 第一天 二叉查找树

比普通的dictionary效率还仅仅是快11倍,从数量级来说还不是非常明显,为什么说不是非常明显,这是因为普通的查找树的时间复杂度

不是严格的log(N),在最坏的情况下会出现“链表”的形式,复杂度退化到O(N),比如下图。

     6天通吃树结构—— 第一天 二叉查找树

不过总会有解决办法的,下一篇我们继续聊如何旋转,保持最坏复杂度在O(logN)。

上一篇:linux expect远程自动登录以及执行命令


下一篇:《Adobe InDesign CS5中文版经典教程》—第1课1.9节查找InDesign帮助资源