查找算法之顺序、二分、二叉搜索树、红黑树 详细比较总结 阅读 5969

前言

一般用符号表来储存键值对,就好像字典那样,通过索引来查找值,若键重复则覆盖值。我们能希望找到一种高效的查找算法使在平均情况和最差情况下,时间复杂度都能达到O(logn)。下面会逐步介绍四种算法,最终达到我们的目的。

顺序查找

用链表实现,无法索引数据,必须遍历找数据,速度比较慢,查找插入时间复杂度都为O(n),而且无法保证有序。但是实现简单,适用于小型数据。

public class SequentialSearchST  {    private Node head;    private int size=0;    public void put(Key key,Value v){        Node p=head;        while(p!=null){            if(p.key.equals(key)){                p.v=v;                return;            }            p=p.next;        }        head=new Node(key,v,head);        size++;    }    public Value get(Key key){        Node p=head;        while (p!=null){            if(p.key.equals(key)){                return  p.v;            }            p=p.next;        }        return null;    }}

二分查找

用数组保存数据,保证有序。二分查找速度很快,但是仅限于查找。因为插入的时候要保证有序,所以要往后移动数据以便插入。查找复杂度O(logn),插入复杂度O(n)。

public class BinarySearch {    public void put(Key key,Value value){        int index=rank(key);        //键相等则覆盖值        if(keys[index]!=null&&key.compareTo(keys[index])==0){            values[index]=value;            return;        }        //把数据往后移,以便插入        for(int i=size+1;i>index;i--){            keys[i]=keys[i-1];            values[i]=values[i-1];        }        keys[index]=key;        values[index]=value;        size++;    }    public Value get(Key key){        int index=rank(key);//二分查找        if(keys[index]!=null && key.compareTo(keys[index])==0){            return values[index];        }        return null;    }      public int rank(Key key){return rank(key,0,size);}    public int rank(Key key,int l,int h){        if(l>h) return l;        int mid = (l+h)/2;        int cmp=0;        if(keys[mid]!=null)            cmp=key.compareTo(keys[mid]);        if(cmp<0)            return rank(key,l,mid-1);        else if(cmp>0)            return rank(key,mid+1,h);        return mid;    }}

二叉搜索树

通过前面两个算法,我们可以知道链表能快速删除插入,而二分能快速查找。所以我们想找到一种结构既是链式结构,同时又能进行二分查找,同时保证查找和插入的高效性。

答案就是二叉搜索树。

定义

是二叉树

每个节点含有一个键和关联的值

且每个节点的键大于左儿子且小于右儿子

实现

其实给出定义,实现就已经很清楚了。说白了就是从无到有构造一个二叉树,每次插入都和树中的节点进行比较,小的放左边,大的放右边。就如同快速排序,用一个主元把左右两边分开。

还是直接看代码清楚点

public class BST{    Node root;    public void put(Key key,Value value){        root = put(root,key,value);    }    public Node put(Node x, Key key, Value value) {        if(x==null){            return new Node(key,value,0);        }        int cmp = key.compareTo(x.key);        if(cmp<0) x.left=put(x.left,key,value);        else if(cmp>0) x.right=put(x.right,key,value);        else {            x.value=value;            x.N = size(x.right)+size(x.left)+1;        }        return x;    }    public Value get(Key key){        return get(root,key);    }    private Value get(Node x, Key key) {        if(x==null)            return null;        int cmp =key.compareTo(x.key);        if(cmp<0)  return get(x.left,key);        else if(cmp>0) return get(x.right,key);        return x.value;    }}

效率问题

二叉搜索树的查找和搜索在平均情况下时间复杂度都能达到O(logn),而且能保证数据有序。二叉搜索树的中序遍历就是数据的顺序。我们貌似已经找到了一个最理想的算法。

但是这个效率只是在平均情况下。如果数据是逆序,或者顺序,那么这棵树就会发生一边倒的情况使复杂度直接达到O(n),就如同快排中选择到糟糕的主元(最大或者最小)。比快排糟糕的是,快排我们能通过随机打乱数据来避免这种情况发生。但二叉搜索树则不行,数据都是客户提供,直接插入到树中的,这种情况其实经常发生。

幸运的是我们有平衡二叉树可以解决这个问题。

平衡二叉树

2-3树

为了保持树平衡性,允许节点能保存两个键值对,且能连三个儿子。这样把节点分成了两种类型。

2-节点 : 一个键值对,两个儿子。 (也就是标准的二叉搜索树)

3-节点 : 两个键值对,三个儿子。 (两个键是有序的,左小右大。左儿子小于左边的键,右儿子大于右边的键,中间的儿子在两个键之间)

实现原理

2-3树插入比较复杂。在插入的同时保持平衡性。

向2-节点中插入键。这种情况比较简单。直接插入即可。

向3-节点中插入键。比较特殊。先暂时把键插入到3-节点,此时这个节点中就有了三个键,然后再把这个节点分开。把中间的儿子简单当根,左右两边的键当儿子。若父节点还是3-节点,则继续递归进行。

性能分析

2-3树性能可以说是比较好的。不管数据怎么样,查找删除操作时间复杂度都能达到O(logn)。

但是2-3树实现比较复杂,需要掌控的情况很多,剥离节点,传递节点等操作,都需要很复杂的代码,且也会耗费不少的时间。所以我们一般不怎么用原始的2-3树,而是用2-3树的变形红黑树.

红黑树

红黑树最方便的地方除了插入和删除操作的代码略复杂以外,另外的操作都可以直接复制二叉搜索树。

红黑树是2-3树的变形,把3-节点分离开来使之成为普通的2-节点。但是怎么表现分离开的节点之间的联系呢。我们用红线把他们连起来。

巧妙地结合二叉搜索树的高效查找操作和2-3树的平衡插入操作。

每个节点都只有一根连向父节点的线。这个线的颜色称为节点的颜色。

实现

我们通过旋转来维持树的平衡。一般有两种情况需要旋转。

连续两个左节点的颜色为红色,向右转

右节点的颜色为红色,向左转

第三种情况是左右两边都为红色。最好处理,不需要旋转。只需要把左右两个儿子的颜色改成黑色,再把自己的颜色改成黑色。可以想象成把3个键值对3-节点剥离开。

public class BlackRedBST {    private final boolean RED = true;    private final boolean BLACK = false;    private Node root;    public void put(Key key,Value value){        root = put(root,key,value);        root.color = BLACK;    }    private Node put(Node x, Key key, Value value) {        if(x==null) return new Node(key,value,0,RED);        int cmp = key.compareTo(x.key);        if(cmp<0) x.left = put(x.left,key,value);        else if(cmp>0) x.right = put(x.right,key,value);        else if(cmp==0) x.value =value;        if( isRED(x.right) && !isRED(x.left)) x=rotateLeft(x);        if( isRED(x.left) && isRED(x.left.left)) x=rotateRight(x);        if( isRED(x.left) && isRED(x.right)) flipColor(x);        x.N = size(x.right) + size(x.left) +1;        return  x;    }    private void flipColor(Node x) {        x.right.color = BLACK;        x.left.color = BLACK;        x.color = RED;    }    private Node rotateLeft(Node x) {        Node r =x.right;        x.right = r.left;        r.left = x;        r.color = x.color;        x.color = RED;        x.N = size(x.left)+size(x.right) +1;        return r;    }    private Node rotateRight(Node x) {        Node r =x.left;        x.left = r.right;        r.right = x;        r.left.color = RED;        r.right.color = RED;        r.color =BLACK;        x.N = size(x.left)+size(x.right) +1;        return r;    }}

性能分析

无论数据如何,插入删除时间复杂度都为O(logn),可以说达到了理想状态,且代码简单。

性能测试

分别对四个文件进行插入搜索操作。

tale.txt(779kb)

顺序查找(7.143秒) 二分查找(0.46秒) 二叉搜索树(0.191秒) 红黑树(0.237秒)

leipzig100k.txt(12670kb)

顺序查找(无) 二分查找(13.911秒) 二叉搜索树(1.389秒) 红黑树(1.442秒)

leipzig300k.txt(37966kb)

顺序查找(无) 二分查找(60.222秒) 二叉搜索树(2.742秒) 红黑树(3.104秒)

leipzig1m.txt(126607kb)

顺序查找(无) 二分查找(无) 二叉搜索树(3.016秒) 红黑树(2.797秒)

由上面的数据分析,顺序查找实际是非常慢的。而二分查找对小型数据还是比较快,但是数据一大就不行了。

而这里的二叉搜索树和红黑树,无论什么数据效率都是极高。而且由leipzig300k.txt到leipzig1m.txt数据几乎翻了4倍,而这两种算法的效率几乎没收什么影响。

这里因为我的数据比较平均的关系,比较不出红黑树和二叉搜索树的差异。我自己构造了一组数据进行测试。完全逆序的100000个数进行插入删除。

红黑树(0.173秒)

二叉搜索树(StackOverflow)

Reference

维基百科

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 红黑树是平衡二叉查找树的一种。为了深入理解红黑树,我们需要从二叉查找树开始讲起。 BST 二叉查找树(Binary...
    kanehe阅读 1,363评论 0 8
  • 如需转载, 请咨询作者, 并且注明出处.有任何问题, 可以关注我的微博: coderwhy, 或者添加我的微信: ...
    coderwhy阅读 8,664评论 12 35
  • 一、基本数据类型 注释 单行注释:// 区域注释:/* */ 文档注释:/** */ 数值 对于byte类型而言...
    龙猫小爷阅读 4,213评论 0 16
  • 体验:今天终于把速腾修好了,上路试车变速箱没有问题,明天把前轮轴承换掉,就完工了,核心:有问题的车,就不能上路,转...
    郭家乐阅读 104评论 0 0
  • 浑浑噩噩的过了小学, 后来懵懵懂懂的竟然进了学校里的尖子班,虽然后来又打下到普通班, 现在高中,进了县城里最差的高...
    眼里的星空阅读 217评论 4 2