数据结构与算法(五)--- 哈希表、树、二叉树的入门

一、哈希表

(一)哈希表的定义

哈希表(Hash table,也叫散列表) 是根据关键码值(Key value)而直接进行访问的数据结构,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。
关键码值(Key value)也可以当成是key的hash值
这个映射函数叫做散列函数
存放记录的数组叫做散列表

数组,链表,哈希表的区别:

  • 数组(顺序表):寻址容易,只要使用下标即可;插入和删除难,会有大量的元素进行移动
  • 链表:插入与删除容易,但是查询难
  • 哈希表:寻址容易,插入删除也容易,综合了数组和链表的特性。

(二)哈希表的图解

Key : {14, 19, 5, 7, 21, 1, 13, 0, 18} 散列表: 大小为13 的数组 a[13]; 散列函数: f(x) = x mod 13; 遇到hash冲突则hash值+1(直到加到没有hash冲突的位置,此处先用简单的方式来解决hash冲突)

  • 散列表的大小:是需要根据自己的需求而定的
  • 散列函数:为了key计算后的结果hash值,能够不超出定义的散列表的大小
  • hash冲突:不同的key通过散列函数计算的hash值一样。自定义的哈希表需要设计好hash冲突解决的方法。
  • 装填因子:放入散列表中的数据个数 / 散列表的大小
    [1] 假设装填因子=0.7,散列表中的数据个数 / 散列表的大小 = 9/13 = 0.7,如果散列表中数据个数达到了9个,就需要对散列表进行扩容,扩容后再存放新的数据。HashMap设置的装填因子是0.75。
    [2] 那么为什么不全部放满了再扩容呢,需要根据装填因子?因为数据越接近散列表的大小,产生冲突的可能会越来越大
  • 缺点:扩容需要消耗大量的空间和性能。
    扩容会导致散列函数重新变化,[ 比如扩容后大小为50,那么f(x) = x mod 50 ],原本存入哈希表的key值都需要重新计算。
    HashMap的扩容就是2的n次方计算的。
  • 应用:散列表应用场景,是在数组大小变化的可能性不大的情况下:电话号码,字典,点歌系统,QQ,微信的好友等。
    比如系统的电话号码存放有上限值的,这样增删改查就能保持性能最快的一种方式。

数据结构与算法(五)--- 哈希表、树、二叉树的入门_第1张图片

(三)哈希表的拉链法

要能够手写一套性能优异的哈希表,需要了解所有的树结构。不同的语言,设计的哈希表也是有些差异的,HashMap就是采用了拉链法。

1、JDK1.8以前:数组 + 链表

优点:

  • 查找快:时间复杂度 O(n) ,( O(n/线性表的size) ) ,是一个线性的查找
    [e.g.] f(337) = 1,查找到数组角标1,然后再单链表查询到337。
  • 插入快:查找到数组角标,单链表的插入
  • 删除快:查找到数组角标,单链表的删除


缺点:

  • 大数据时代,如果数组容量不是很大,很有可能出现某一行链表个数超过几万几十万。(解决方法见数组 + 链表 + 红黑树)

2、JDK1.8以后:数组 + 链表 + 红黑树 (大数据时代)

阈值:链表长度最长的值。
当链表长度超过阈值,就转换成红黑树。
HashMap从JDK1.8以后采用了这种方式,更适应大数据时代。

红黑树和链表对比(如果查找第1000个数据):

  • 链表:需要查询1000次才能查找到
  • 红黑树:每次都是两个两个找,查找的次数:log21000 = 9次 (210=1024)
    也就是寻找的力度越大,查找的速度就会越快

二、树的入门

(一)什么是树,森林

1、树(Tree)是n(≥0)个结点的有限集。n=0时成为空树。
在任意一颗非空树中:

  • 有且仅有一个特定的称为根(Root)的结点
  • 当n>1时,其余结点可分为m(m>0)个互不相交的有限集 T1、T2、…Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)
    (一个结点只会有一个父亲)


2、森林(Forest)是m(m≥0)棵互不相交的树的集合。

【树的图示】:

【辨别以下哪些为树】:
数据结构与算法(五)--- 哈希表、树、二叉树的入门_第2张图片

(二)树的概念

1、结点与树的度

  • 结点的度:结点拥有的子树数
  • 叶子结点(终端结点):度为0的结点
  • 分支结点(非终端结点):度不为0的结点
  • 内部结点:除根结点以外的分支结点
  • 树的度:树内各结点的度的最大值

2、层次和深度

  • 结点的层次(Level):从根开始定义起,根为第一层,根的孩子为第二层。若某结点在第L层,则其子树的跟就在L+1层。其双亲在同一层的结点互为堂兄弟。
  • 树的深度(Depth)或高度:树中结点的最大层次。

(三)树的存储结构

1、双亲表示法 (少用)

平时会很少用到,启发式寻路会使用到双亲表示法

2、孩子表示法(常用)

  • 【方法一】树的度最大值为N,那么每个结点都会带N个指针,在使用的时候非常消耗空间。
    二叉树采用的是这种方式:因为二叉树最多只有两个度,所以空间浪费没有这么多。
    数据结构与算法(五)--- 哈希表、树、二叉树的入门_第3张图片
  • 【方法二】标记总共有N个孩子,那么每个结点都会带N个指针:多叉树可以采用这种方式
    数据结构与算法(五)--- 哈希表、树、二叉树的入门_第4张图片

3、双亲孩子表示法(少用)

把每个结点的孩子结点排列起来,以单链表作为存储结构, 则n个结点有n个孩子链表,如果是叶子结点则此单链表为空, 然后n个头指针又组成一个线性表,采用顺序存储结构,存放在一个一维数组中

  • 父亲只有一个,用数组存放
  • 孩子有很多,用单链表存放


和HashMap有点相似,但是底层结构完全不一样。

数据结构与算法(五)--- 哈希表、树、二叉树的入门_第5张图片

4、孩子兄弟表示法(少用)

孩子兄弟表示法为每个节点设计三个域: 一个数据域,一个该节点的第一个孩子节点域,一个该节点的下一个节点的兄弟指针域

数据结构与算法(五)--- 哈希表、树、二叉树的入门_第6张图片

三、二叉树的入门

(一)二叉树的定义

1、二叉树

二叉树(Binary Tree)是n(≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
度:最大为2。
[e.g.] 三维建模,各种优异的算法能解决实际问题的都是采用二叉树。

     数据结构与算法(五)--- 哈希表、树、二叉树的入门_第7张图片

2、斜树

数据结构与算法(五)--- 哈希表、树、二叉树的入门_第8张图片

3、满二叉树

除最后一层无任何子节点外,每一层上的所有结点都有两个子结点二叉树

数据结构与算法(五)--- 哈希表、树、二叉树的入门_第9张图片

4、完全二叉树

完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。
(只缺少最右边过来的子二叉树,编号是连续的)
完全二叉树是满二叉树的子集。

数据结构与算法(五)--- 哈希表、树、二叉树的入门_第10张图片
【以下哪些是完全二叉树】:就看编号是否连续的
数据结构与算法(五)--- 哈希表、树、二叉树的入门_第11张图片

(二)二叉树的存储结构

1、顺序存储(少用)

  • 将每个结点存放到数组中
  • 如果结点为空,则存放null

数据结构与算法(五)--- 哈希表、树、二叉树的入门_第12张图片

2、链式存储(常用)

数据结构与算法(五)--- 哈希表、树、二叉树的入门_第13张图片

(三)二叉树的遍历

1、树的创建

/**
 * 功能:二叉树
 * 

* Created by danke on 2018/12/3. */ public class BinaryTree { transient Node root; // 树的根结点 public BinaryTree(E e) { this.root = new Node<>(e, null, null); } /** * 创建二叉树 * @param root */ public void createTree(Node root) { // 创建每个结点 Node bNode = new Node<>("B", null, null); Node cNode = new Node<>("C", null, null); Node dNode = new Node<>("D", null, null); Node eNode = new Node<>("E", null, null); Node fNode = new Node<>("F", null, null); Node gNode = new Node<>("G", null, null); Node hNode = new Node<>("H", null, null); Node iNode = new Node<>("I", null, null); // 设置每个结点的左右孩子 root.leftChild = bNode; root.rightChild = cNode; bNode.leftChild = dNode; dNode.leftChild = gNode; dNode.rightChild = hNode; cNode.leftChild = eNode; cNode.rightChild = fNode; eNode.rightChild = iNode; } /** * 结点 */ public class Node { E data; Node leftChild; // 左孩子 Node rightChild; // 右孩子 public Node(E data, Node leftChild, Node rightChild) { this.data = data; this.leftChild = leftChild; this.rightChild = rightChild; } } }

2、前序遍历 DLR

规则是若二叉树为空,则空操作返回,否则先访问跟结点,然后前序遍历左子树,再前序遍历右子树(根—左---右)

[e.g.] 快速排序就是标准的前序遍历。 Arrays.sort();

/**
 * 前序遍历 DLR
 * @param root
 */
public void preOrderTraverse(Node root) {
    if (root == null) {
        return;
    }
    System.out.print(root.data); // 根 -- 输出
    preOrderTraverse(root.leftChild); // 左 -- 逻辑
    preOrderTraverse(root.rightChild); // 右 -- 逻辑
}

3、中序遍历 LDR

规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点), 中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树(左–根--右)

[e.g.]汉诺塔问题(可以看一下上一章递归)

/**
 * 中序遍历 LDR(采用了递归的方式)
 * @param root
 */
public void midOrderTraverse(Node root) {
    if (root == null) {
        return;
    }
    midOrderTraverse(root.leftChild); // 左 -- 逻辑
    System.out.print(root.data); // 根 -- 输出
    midOrderTraverse(root.rightChild); // 右 -- 逻辑
}

4、后序遍历 LRD

规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点(左—右---根)

/**
 * 后序遍历 LRD
 * @param root
 */
public void postOrderTraverse(Node root) {
    if (root == null) {
        return;
    }
    postOrderTraverse(root.leftChild); // 左 -- 逻辑
    postOrderTraverse(root.rightChild); // 右 -- 逻辑
    System.out.print(root.data); // 根 -- 输出
}

你可能感兴趣的:(数据结构与算法)