转载和引用,请注明原文出处! Fork me on GitHub
结局很美妙的事,开头并非如此!

InterV 6:数据结构

一. Queue(队列)

1. 什么是队列

队列是数据结构中比较重要的一种类型,它支持 FIFO,尾部添加、头部删除(先进队列的元素先出队列),队列结构与日常生活中排队等候服务的模型是一致的,像火车站买票,早排队进入队列的人,早买到票并最先离开(出队);后到来的人只能排在队列的后,后得到服务并后离开。

2. 队列的种类

  • 单队列(单队列就是常见的队列, 每次添加元素时,都是添加到队尾,存在“假溢出”的问题也就是明明有位置却不能添加的情况)
  • 循环队列(避免了“假溢出”的问题)

3. 队列-顺序存储方式:

在队列的顺序存储实现中,我们可以将队列当作一般的表用数组加以实现,但这样做的 效果并不好。尽管我们可以用一个指针 last 来指示队尾,使得 enqueue 运算可在Ο(1)时间内 完成,但是在执行 dequeue 时,为了删除队首元素,必须将数组中其他所有元素都向前移动 一个位置。这样,当队列中有 n 个元素时,执行 dequeue 就需要Ο(n)时间。

为了提高运算的效率,我们用另一种方法来表达数组中各单元的位置关系。设想数组 A[0.. capacity-1]中的单元不是排成一行,而是围成一个圆环

 

可看代码例子QueueTest.java

 

4. 队列-链式存储方式:

队列的链式存储可以使用单链表来实现。为了操作实现方便,这里采用带头结点的单链 表结构。根据单链表的特点,选择链表的头部作为队首,链表的尾部作为队尾。除了链表头 结点需要通过一个引用来指向之外,还需要一个对链表尾结点的引用,以方便队列的入队操 作的实现。为此一共设置两个指针,一个队首指针和一个队尾指针,如图所示,队首指针指(front)向队首元素的前一个结点,即始终指向链表空的头结点; 队尾指针(rear)指向队列当前队尾元素所在的结点。当队列为空时,队首指针与队尾指针均指向空的头结点

在Java中没有显式的指针类型,然而实际上对象的访问就是使用指针来实现的,即在Java中是使用对象的引用来替代指针的。

 

 

在节点中数据域用来存储数据元素,指针域用于指向下一个具有相同结构的节点

SLNode类:由data和next指针组成。

QueueSLinked类:出队入队实现

在Java中没有显式的指针类型,然而实际上对象的访问就是使用指针来实现的,即在Java中是使用对象的引用来替代指针的。因此在使用Java实现该节点结构时,一个节点本身就是一个对象。节点的数据域data可以使用一个Object类型的对象来实现,用于存储任何类型的数据元素,并通过对象的引用指向该元素;而指针域next可以通过节点对象的引用来实现

5. Java 集合框架中的队列 Queue

Java 集合中的 Queue 继承自 Collection 接口 ,Deque, LinkedList, PriorityQueue, BlockingQueue 等类都实现了它。 Queue 用来存放等待处理元素的集合,这种场景一般用于缓冲、并发访问。 除了继承 Collection 接口的一些方法,Queue 还添加了额外的 添加、删除、查询操作。

二. Set

1. 什么是 Set

Set 继承于 Collection 接口,是一个不允许出现重复元素,并且无序的集合,主要 HashSet 和 TreeSet 两大实现类。

在判断重复元素的时候,Set 集合会调用 hashCode()和 equal()方法来实现。

补充:有序集合与无序集合说明

  • 有序集合:集合里的元素可以根据 key 或 index 访问 (List、Map)
  • 无序集合:集合里的元素只能遍历。(Set)

2. HashSet 和 TreeSet 底层数据结构

HashSet 是哈希表结构,主要利用 HashMap 的 key 来存储元素,计算插入元素的 hashCode 来获取元素在集合中的位置;

TreeSet 是红黑树结构,每一个元素都是树中的一个节点,插入的元素都会进行排序;

三、List

1. 什么是List

在 List 中,用户可以精确控制列表中每个元素的插入位置,另外用户可以通过整数索引(列表中的位置)访问元素,并搜索列表中的元素。 与 Set 不同,List 通常允许重复的元素。 另外 List 是有序集合而 Set 是无序集合。

2. List的常见实现类

ArrayList 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。

LinkedList 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率高。

它每一个节点(Node)都包含两方面的内容:

1.节点本身的数据(data);

2.下一个节点的信息(nextNode)。 

所以当对LinkedList做添加,删除动作的时候就不用像基于数组的ArrayList一样,必须进行大量的数据移动。只要更改nextNode的相关信息就可以实现了,这是LinkedList的优势。

Vector 是矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。但是ArrayList是非线程安全的,而Vector是线程安全的。

Stack 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。

总结:

大多数情况下,从性能上来说ArrayList最好,但是当集合内的元素需要频繁插入、删除时LinkedList会有比较好的表现,但是它们三个性能都比不上数组,另外Vector是线程同步的。所以: 
如果能用数组的时候(元素类型固定,数组长度固定),请尽量使用数组来代替List; 
如果没有频繁的删除插入操作,又不用考虑多线程问题,优先选择ArrayList; 
如果在多线程条件下使用,可以考虑Vector; 
如果需要频繁地删除插入,LinkedList就有了用武之地; 
如果你什么都不知道,用ArrayList没错。 

 

所有的List中只能容纳单个不同类型的对象组成的表,而不是Key-Value键值对。例如:[ tom,1,c ]

所有的List中可以有相同的元素,例如Vector中可以有 [ tom,koo,too,koo ]

所有的List中可以有null元素,例如[ tom,null,1 ]

 基于Array的List(Vector,ArrayList)适合查询,而LinkedList 适合添加,删除操作

3. ArrayList 和 LinkedList 源码学习

4. 推荐阅读

四. Map

1HashMap: 元素成对,元素可为空

2HashTable: 元素成对,线程安全,元素不可为空 

五、树

1. 树的基本概念

树是由一个集合以及在该集合上定义的一种关系构成的。集合中的元素称为树的结点, 所定义的关系称为父子关系。父子关系在树的结点之间建立了一个层次结构。在这种层次结构中有一个结点具有特殊的地位,这个结点称为该树的根结点,或简称为树根

树(Tree)是n(n>=0)个结点的有限集。n=0时称为空树。

在任意一颗非空树中

(1)有且仅有一个特定的称为根(Root)的结点。

(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、.....、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。

下图就符合树的定义:

 

mark

其中根结点A有两个子树:

markmark

需要注意的是虽然子树的个数没有限制,但是它们一定是互不交互的。下面的图明显不符合互不交互的原则,所以不是树。

 

markmark

树的结点

树的结点包含一个数据元素及若干指向其子树的分支。结点拥有的子树数称为结点的度(Degree)树的度是树内各结点度的最大值。

mark

mark

结点的层次从根开始定义起,根为第一层,根的孩子为第二层,以此类推。树的深度(Depth)或高度是树中结点的最大层次。

mark

树的存储结构

mark

 

2. 二叉树定义

每个结点的度均不超过 2 的有序树,称为二叉树(binary tree)。

二叉树(Binary Tree)是n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成(子树也为二叉树)。

二叉树的特点

  • 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点
  • 左子树和右子树是有顺序的,次序不能任意颠倒。
  • 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。

二叉树五种基本形态

  1. 空二叉树
  2. 只有一个根结点
  3. 根结点只有左子树
  4. 根结点只有右子树
  5. 根结点既有左子树又有右子树

几种特殊的二叉树

斜树

mark

左斜树:mark右斜树:mark

满二叉树

mark

满二叉树:mark

完全二叉树

mark

完全二叉树:mark

二叉树的性质

二叉树性质1

性质1:在二叉树的第i层上至多有2i-1个结点(i>=1)

二叉树性质2

性质2:深度为k的二叉树至多有2k-1个结点(k>=1)

二叉树性质3

性质3:对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2+1。

一棵二叉树,除了终端结点(叶子结点),就是度为1或2的结点。假设n1度为1的结点数,则数T 的结点总数n=n0+n1+n2。我们再换个角度,看一下树T的连接线数,由于根结点只有分支出去,没有分支进入,所以连接线数为结点总数减去1。也就是n-1=n1+2n2,可推导出n0+n1+n2-1 = n1+2n2,继续推导可得n0 = n2+1。

二叉树性质4

性质4:具有n个结点的完全二叉树的深度为[log2n ] + 1([X]表示不大于X的最大整数)。

由性质2可知,满二叉树的结点个数为2k-1,可以推导出满二叉树的深度为k=log2(n + 1)。对于完全二叉树,它的叶子结点只会出现在最下面的两层,所以它的结点数一定少于等于同样深度的满二叉树的结点数2k-1,但是一定多于2k-1 -1。因为n是整数,所以2k-1 <= n < 2k,不等式两边取对数得到:k-1 <= log2n <k。因为k作为深度也是整数,因此 k= [log2n ]+ 1。

二叉树性质5

性质5:如果对一颗有n个结点的完全二叉树(其深度为[log2n ] + 1)的结点按层序编号(从第1层到第[log2n ] + 1层,每层从左到右),对任一结点i(1<=i<=n)有:

  1. 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2]。

  2. 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点i。
  3. 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。

结合下图很好理解:

mark

3. 二叉树数据结构

链式存储结构

二叉树的存储结构

二叉树顺序存储结构

 

mark

^代表不存在的结点。

对于右斜树,顺序存储结构浪费存储空间:

 

mark

二叉树的顺序存储结构缺点很明显:不能反应逻辑关系;对于特殊的二叉树(左斜树、右斜树),浪费存储空间。所以二叉树顺序存储结构一般只用于完全二叉树。

 

二叉链表

链表每个结点包含一个数据域和两个指针域:

mark

其中data是数据域,lchild和rchild都是指针域,分别指向左孩子和右孩子。

 

mark

 

二叉树的二叉链表结点结构定义:

 

/*二叉树的二叉链表结点结构定义*/
typedef struct BiNode
{
    char data;      /*结点数据*/
    struct BiNode *lchild, *rchild;     /*左右孩子指针*/
}BiNode,*BiTree;

 

4. 如何初始化二叉树及遍历?

二叉树遍历:从树的根节点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问仅且一次。

这里有两个关键词:访问次序。

二叉树遍历方法

 

mark

 

mark

前序遍历

 

mark

 

递归方式实现前序遍历

具体过程:

  1. 先访问根节点
  2. 再序遍历左子树
  3. 最后序遍历右子树

代码实现:

public static void PreOrderRecur(TreeNode<char> treeNode)
 {
     if (treeNode == null)
     {
         return;
     }
     Console.Write(treeNode.Data); 
     PreOrderRecur(treeNode.LChild);
     PreOrderRecur(treeNode.RChild);
 }

非递归方式实现前序遍历

具体过程:

  1. 首先申请一个新的栈,记为stack;
  2. 将头结点head压入stack中;
  3. 每次从stack中弹出栈顶节点,记为cur,然后打印cur值,如果cur右孩子不为空,则将右孩子压入栈中;如果cur的左孩子不为空,将其压入stack中;
  4. 重复步骤3,直到stack为空.

代码实现:

 public static void PreOrder(TreeNode<char> head)
{
    if (head == null)
    {
        return;
    }
    Stack<TreeNode<char>> stack = new Stack<TreeNode<char>>();
    stack.Push(head);
    while (!(stack.Count == 0))
    {
        TreeNode<char> cur = stack.Pop();
        Console.Write(cur.Data);

        if (cur.RChild != null)
        {
            stack.Push(cur.RChild);
        }
        if (cur.LChild != null)
        {
            stack.Push(cur.LChild);
        }
    }
}

过程模拟:

执行结果:mark

 

中序遍历

 

mark

递归方式实现中序遍历

具体过程:

  1. 先中序遍历左子树
  2. 再访问根节点
  3. 最后中序遍历右子树

代码实现:

public static void InOrderRecur(TreeNode<char> treeNode)
{
    if (treeNode == null)
    {
        return;
    }  
    InOrderRecur(treeNode.LChild);
    Console.Write(treeNode.Data); 
    InOrderRecur(treeNode.RChild);
}

非递归方式实现中序遍历

具体过程:

  1. 申请一个新栈,记为stack,申请一个变量cur,初始时令cur为头节点;
  2. 先把cur节点压入栈中,对以cur节点为头的整棵子树来说,依次把整棵树的左子树压入栈中,即不断令cur=cur.left,然后重复步骤2;
  3. 不断重复步骤2,直到发现cur为空,此时从stack中弹出一个节点记为node,打印node的值,并让cur = node.right,然后继续重复步骤2;
  4. 当stack为空并且cur为空时结束。

代码实现:

public static void InOrder(TreeNode<char> treeNode)
{
    if (treeNode == null)
    {
        return;
    }
    Stack<TreeNode<char>> stack = new Stack<TreeNode<char>>();

    TreeNode<char> cur = treeNode;

    while (!(stack.Count == 0) || cur != null)
    {
        while (cur != null)
        {
            stack.Push(cur);
            cur = cur.LChild;
        }
        TreeNode<char> node = stack.Pop();
        Console.WriteLine(node.Data);
        cur = node.RChild;
    }
}

过程模拟:

执行结果:mark

 

后序遍历

 

mark

 

递归方式实现后序遍历

  1. 先后序遍历左子树
  2. 再后序遍历右子树
  3. 最后访问根节点

代码实现:

public static void PosOrderRecur(TreeNode<char> treeNode)
{
    if (treeNode == null)
    {
        return;
    }
    PosOrderRecur(treeNode.LChild);
    PosOrderRecur(treeNode.RChild);
    Console.Write(treeNode.Data); 
}

非递归方式实现后序遍历一

具体过程:

使用两个栈实现

  1. 申请两个栈stack1,stack2,然后将头结点压入stack1中;
  2. 从stack1中弹出的节点记为cur,然后先把cur的左孩子压入stack1中,再把cur的右孩子压入stack1中;
  3. 在整个过程中,每一个从stack1中弹出的节点都放在第二个栈stack2中;
  4. 不断重复步骤2和步骤3,直到stack1为空,过程停止;
  5. 从stack2中依次弹出节点并打印,打印的顺序就是后序遍历的顺序;

代码实现:

public static void PosOrderOne(TreeNode<char> treeNode)
{
    if (treeNode == null)
    {
        return;
    }

    Stack<TreeNode<char>> stack1 = new Stack<TreeNode<char>>();
    Stack<TreeNode<char>> stack2 = new Stack<TreeNode<char>>();

    stack1.Push(treeNode);
    TreeNode<char> cur = treeNode;

    while (!(stack1.Count == 0))
    {
        cur = stack1.Pop();
        if (cur.LChild != null)
        {
            stack1.Push(cur.LChild);
        }
        if (cur.RChild != null)
        {
            stack1.Push(cur.RChild);
        }
        stack2.Push(cur);
    }

    while (!(stack2.Count == 0))
    {
        TreeNode<char> node = stack2.Pop();
        Console.WriteLine(node.Data); ;
    }
}

过程模拟:

执行结果:mark

非递归方式实现后序遍历二

具体过程:

使用一个栈实现

  1. 申请一个栈stack,将头节点压入stack,同时设置两个变量 h 和 c,在整个流程中,h代表最近一次弹出并打印的节点,c代表当前stack的栈顶节点,初始时令h为头节点,,c为null;

  2. 每次令c等于当前stack的栈顶节点,但是不从stack中弹出节点,此时分一下三种情况:

(1)如果c的左孩子不为空,并且h不等于c的左孩子,也不等于c的右孩子,则吧c的左孩子压入stack中

(2)如果情况1不成立,并且c的右孩子不为空,并且h不等于c的右孩子,则把c的右孩子压入stack中;

(3)如果情况1和2不成立,则从stack中弹出c并打印,然后令h等于c;

  1. 一直重复步骤2,直到stack为空.

代码实现:

public static void PosOrderTwo(TreeNode<char> treeNode)
{
    if (treeNode == null)
    {
        return;
    }

    Stack<TreeNode<char>> stack = new Stack<TreeNode<char>>();
    stack.Push(treeNode);

    TreeNode<char> h = treeNode;
    TreeNode<char> c = null;
    while (!(stack.Count == 0))
    {
        c = stack.Peek();
        //c结点有左孩子 并且 左孩子没被遍历(输出)过 并且 右孩子没被遍历过
        if (c.LChild != null && h != c.LChild && h != c.RChild)
            stack.Push(c.LChild);
        //c结点有右孩子 并且 右孩子没被遍历(输出)过
        else if (c.RChild != null && h != c.RChild)
            stack.Push(c.RChild);
        //c结点没有孩子结点 或者孩子结点已经被遍历(输出)过
        else
        {
            TreeNode<char> node = stack.Pop();
            Console.WriteLine(node.Data);
            h = c;
        }
    }
}

过程模拟:

执行结果:mark

 

层序遍历

 

mark

具体过程:

  1. 首先申请一个新的队列,记为queue;
  2. 将头结点head压入queue中;
  3. 每次从queue中出队,记为node,然后打印node值,如果node左孩子不为空,则将左孩子入队;如果node的右孩子不为空,则将右孩子入队;
  4. 重复步骤3,直到queue为空。

代码实现:

public static void LevelOrder(TreeNode<char> treeNode)
{
    if(treeNode == null)
    {
         return;
    }
    Queue<TreeNode<char>> queue = new Queue<TreeNode<char>>();
    queue.Enqueue(treeNode);

    while (queue.Any())
    {
        TreeNode<char> node = queue.Dequeue();
        Console.Write(node.Data);

        if (node.Left != null)
        {
            queue.Enqueue(node.Left);
        }

        if (node.Right != null)
        {
            queue.Enqueue(node.Right);
        }
    }
}

执行结果:mark

 

7. 堆

数据结构之堆的定义

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

8. 二叉查找树(BST)

浅谈算法和数据结构: 七 二叉查找树

二叉查找树的特点:

  1. 若任意节点的左子树不空,则左子树上所有结点的 值均小于它的根结点的值;
  2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  3. 任意节点的左、右子树也分别为二叉查找树;
  4. 没有键值相等的节点(no duplicate nodes)。

9. 平衡二叉树(Self-balancing binary search tree)

平衡二叉树(百度百科,平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等)

10. 红黑树

红黑树的简介:

R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它是一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

红黑树的特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

注意
(01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
(02) 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。

红黑树示意图如下:

 红黑树的应用:

红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。
例如,Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。

参考文章:http://www.cnblogs.com/skywang12345/p/3245399.html

 11. B-,B+,B*树

二叉树学习笔记之B树、B+树、B*树

《B-树,B+树,B*树详解》

《B-树,B+树与B*树的优缺点比较》

B-树(或B树)是一种平衡的多路查找(又称排序)树,在文件系统中有所应用。主要用作文件的索引。其中的B就表示平衡(Balance) 1. B+ 树的叶子节点链表结构相比于 B- 树便于扫库,和范围检索。 2. B+树支持range-query(区间查询)非常方便,而B树不支持。这是数据库选用B+树的最主要原因。 3. B*树 是B+树的变体,B*树分配新结点的概率比B+树要低,空间使用率更高;

12.  LSM 树

[HBase] LSM树 VS B+树

B+树最大的性能问题是会产生大量的随机IO

为了克服B+树的弱点,HBase引入了LSM树的概念,即Log-Structured Merge-Trees。

LSM树由来、设计思想以及应用到HBase的索引

推荐文章:

【图解数据结构】 树

六、图

BFS及DFS

posted @ 2020-11-09 00:32  小不点啊  阅读(136)  评论(0编辑  收藏  举报