数据结构基础总结

一.数据结构的概念

数据结构包含数据和结构,通俗一点就是将数据按照一定的结构组合起来,不同的组合方式会有不同的效率和不同的使用场景,不仅需要考虑如何存储数据,还考虑支持访问和处理数据的操作,这就是数据结构的概念。数据结构是计算机存储和组织数据的方式


二.数据结构的分类

数据结构分类总览


1.按照数据的逻辑结构分

逻辑结构划分是指按照数据对象中数据元素之间的相互关系进行划分。


(1.)集合结构

集合中的元素除了同属于一个集合外他们之间没有其他的关系。 如公共汽车上的所有乘客,存放在仓库中的产品。

集合中的元素无前驱,无后继。


(2.)线性结构

数据结构中线性结构指的是数据元素之间存在着“一对一”的线性关系的数据结构。 常用的线性结构有:线性表,栈,队列,双队列,数组,串。元素除首尾元素外都有一个前驱,一个后继。
线性结构
线性结构的特点:

  1. 集合中必存在唯一的一个"第一个元素";
  2. 集合中必存在唯一的一个"最后的元素";
  3. 除最后元素之外,其它数据元素均有唯一的"后继";
  4. 除第一元素之外,其它数据元素均有唯一的"前驱";


(3.)树形结构

树形结构中的数据元素是一对多的层级关系。常见的树形结构:二叉树、B树、哈夫曼树、红黑树等。元素一个前驱,多个后继。
树结构
树结构的特点:

  • 有且仅有一个根节点;
  • 子树之间不可以有交集;
  • 树分为无序树,有序树,二叉树等;
  • 树的深度指的是树的有多少层;
  • 一个节点的度指的是该节点下有多少个子节点;
  • 二叉树指的是每个结点的度≤2的树。
  • 树的遍历方式分为三种,分别是前序遍历(根左右),中序遍历(左根右),后序遍历(左右根);

二叉树是一种比较有用的折中方案,它添加,删除元素都很快,并且在查找方面也有很多的算法优化,所以,二叉树既有链表的好处,也有数组的好处,是两者的优化方案,在处理大批量的动态数据方面非常有用。 二叉树有很多扩展的数据结构,包括平衡二叉树、红黑树、B+树等,这些数据结构二叉树的基础上衍生了很多的功能,在实际应用中广泛用到,例如mysql的数据库索引结构用的就是B+树,还有HashMap的底层源码中用到了红黑树。这些二叉树的功能强大,但算法上比较复杂,想学习的话还是需要花时间去深入的。
二叉树: 二叉树是有限个结点的集合,这个集合或者是空集,或者是由一个根结点和两株互不相交的二叉树组成,其中一株叫根的做左子树,另一棵叫做根的右子树。


(4.)图形结构

图形结构的数据元素是多对多的关系,图形结构中,每个结点的前驱结点数和后续结点数可以任意多个。数据元素间的关系是任意的。其他数据结构(如树、线性表等)都有明确的条件限制,而图形结构中任意两个数据元素间均可相关联。常用来研究生产流程、施工计划、各种网络建设等问题。元素多个前驱,多个后继。
图结构


2.按照数据的物理结构分

物理结构也叫存储结构,指的是数据的逻辑结构在计算机的存储形式。


(1.)顺序存储结构

将数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一直的。例如编程语言中的数组。现实生活中的例子如排队。
**优点:**随机存取表中元素、储存密度大,节省存储空间。
**缺点:**不便于修改,对结点的插入、删除运算时,可能要移动一系列的结点。
顺序存储结构


(2.)链式存储结构

链式存储结构指的是把数据元素存放在任意的数据单元里,这组存储单元可以是连续的也可以是不连续的。所以这种存储关系并不能反映元素间的物理关系。于是每个元素在存储自己地址的同时还要存储下一个元素的地址(也可以称该地址存储在指针中),通过该地址就可以得到下一个元素。
链式存储结构
链式存储结构特点:

  1. 比顺序存储结构的存储密度小(链式存储结构中每个结点都由数据域与指针域两部分组成,相比顺序存储结构增加了存储空间)。
  2. 逻辑上相邻的节点物理上不必相邻。
  3. 插入、删除灵活 (不必移动节点,只要改变节点中的指针)。
  4. 查找节点时链式存储要比顺序存储慢。
  5. 每个节点是由数据域和指针域组成。
  6. 由于簇是随机分配的,这也使数据删除后覆盖几率降低,恢复可能提高。


(3.)索引存储结构

索引存储结构是除建立存储结点信息外,还建立附加的索引表来标识结点的地址。索引表由若干索引项组成。索引存储方式中索引项的一般形式为:(关键字、地址)。其中,关键字是能够唯一标识一个结点的数据项。

索引存储方式还可以细分为如下两类:
稠密索引(Dense Index):
这种方式中每个结点在索引表中都有一个索引项。其中,索引项的地址指示结点所在的的存储位置;

稀疏索引(Spare Index):
这种方式中一组结点在索引表中只对应一个索引项。其中,索引项的地址指示一组结点的起始存储位置。

索引存储结构的特点:
索引存储结构是用结点的索引号来确定结点存储地址,其优点是检索速度快,缺点是增加了附加的索引表,会占用较多的存储空间。


(4.)散列存储结构(哈希表)

散列表,也叫哈希表,是根据关键码和值 (key和value) 直接进行访问的数据结构,通过key和value来映射到集合中的一个位置,这样就可以很快找到集合中的对应元素,又称hash存储。

散列法存储的基本思想是:由节点的关键码值决定节点的存储地址。散列技术除了可以用于查找外,还可以用于存储。
散列存储结构特点:
散列是数组存储方式的一种发展,相比数组,散列的数据访问速度要高于数组,因为可以依据存储数据的部分内容找到数据在数组中的存储位置,进而能够快速实现数据的访问,理想的散列访问速度是非常迅速的,而不像在数组中的遍历过程,采用存储数组中内容的部分元素作为映射函数的输入,映射函数的输出就是存储数据的位置,这样的访问速度就省去了遍历数组的实现。

哈希表在应用中也是比较常见的,就如Java中有些集合类就是借鉴了哈希原理构造的,例如HashMap,HashTable等,利用hash表的优势,对于集合的查找元素时非常方便的,然而,因为哈希表是基于数组衍生的数据结构,
在添加删除元素方面是比较慢的,所以很多时候需要用到一种数组链表来做,也就是拉链法。拉链法是数组结合链表的一种结构,较早前的hashMap底层的存储就是采用这种结构,直到jdk1.8之后才换成了数组加红黑树的结构.


三.常见的线性结构


1.数组

在程序设计中,为了处理方便,把具有相同类型的若干变量按有序的形式组织起来。这些按序排列的同类数据元素的集合称为数组。数组是可以在内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从0开始。


(1.)数组的特点

  • 长度固定。
  • 既可以存储基本类型,也可以存储引用类型。
  • 存储元素类型一致。
  • 数组可以在内存中连续存储多个元素的构造,在内存中的分配也是连续的。
  • 数组中的元素通过数组的下标进行访问的,下标从0开始。


(2.)数组的优点

  • 按照索引查询元素速度快。
  • 按照索引遍历数组方便。


(3.)数组的缺点

  • 数组的大小固定后就不能扩容了
  • 数组只能存储一种类型的数据
  • 添加,删除的操作慢,因为要移动其他的元素


(4.)数组的适用场景

适合频繁查询,对存储空间要求不大,很少增加和删除的情况.


2.栈

栈是一种只允许在一端进行插入或删除的线性表,也就是说先进后出。栈的操作端通常被称为栈顶,另一端被称为栈底,栈的插入操作称为压栈(push),栈删除操作称为出栈(pop)。压栈是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;出栈则是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。


3.队列

队列与栈一样,也是一种线性表,不同的是,队列可以在一端添加元素,在另一端取出元素,也就是:先进先出。从一端放入元素的操作称为入队,取出元素为出队。


4.链表

链表是一种根据元素节点逻辑关系排列起来的一种数据结构。利用链表可以保存多个数据,这一点类似于数组的概念,但是数组本身有一个缺点—— 数组的长度固定,不可改变,在长度固定的情况下首选的肯定是数组,但是在现实的开发之中往往要保存的内容长度是不确定的,那么此时就可以利用链表这样的结构来代替数组的使用。


(1.)链表的优点

链表是很常用的一种数据结构,不需要初始化容量,可以任意加减元素; 添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快;


(2.)链表的缺点

  • 因为含有大量的指针域,占用空间较大;
  • 查找元素需要遍历链表来查找,非常耗时;


(3.)链表的适用场景

链表适合数据量较小,需要频繁增加,删除操作的场景。


四.哈希表结构


1.数组和链表的缺陷

当频繁的查找数据中的某个元素时,通常会选择数组来存放数据,可以直接通过下标访问数据,但是数组添加元素和删除元素比较麻烦(因为数组的的内存是连续的,添加或者删除元素时位于该元素后的所有元素下标都需要移动)。链表可以解决数组添加和删除数据效率低的问题。但是它查找的效率却很低。哈希表即可以满足快速的获取数据,又可以实现快速的修改和删除数据。

虽然哈希表弥补了数组与链表的不足,但是它是通过牺牲内存来提高性能的。


2.什么是哈希表?

哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数(哈希函数),存放记录的数组叫做散列表(哈希表)。 哈希表hashtable(key,value) 的做法其实很简单,就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,把整型数字当作数组的下标,将value存储在以该数字为下标的数组空间里。而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。 与普通的列表不同的地方在于,普通列表仅能通过下标来获取目标位置的值,而哈希表可以根据给定的key计算得到目标位置的值。


3.哈希算法


(1.)什么是哈希算法

哈希表,其本质是一个数组,在创建哈希表时,初始化一个数组,将元素唯一对应的key映射成该数组中的一个下标,而这个映射过程称之为哈希化,把实现哈希化的函数称之为哈希函数。 哈希函数让key与数组的下标一一对应,由于数组本身支持随机访问,所以,当查找key时,只需要O(1)的查找操作,也就是实现了不经过任何比较,一次便能得到所查记录,这就是哈希表查找速度快的重要原因。 Hash函数非常重要,一个好的Hash函数不仅性能优越,而且还会让存储于底层数组中的值分配的更加均匀,减少冲突发生。之所以是减少冲突,是因为取Hash的过程,实际上是将输入键(定义域)映射到一个非常小的空间中,所以冲突是无法避免的,能做的只是减少Hash碰撞发生的概率。


(2.)常见的哈希化的方式

  • 相加法
    这是最简单的方法,直接将key的每一字符的ASCII值相加,例如abc: 97+98+99=294,但是按照这种方式去计算下标很容易出现重复的下标,而在数组中一个下标只能存储一个值,如果存入后来的值显然会导致之前的值被覆盖。

  • 幂的连乘
    幂的连乘相对于相加法不再那么容易出现下标值相同的情况,那什么是幂的连乘呢?其实,我们平时使用的大于10的数字都是可以以一种幂的连乘的形式来唯一表示,如7582 = 7 * 10³ + 5 * 10² + 8 * 10 + 2。那么字符也可以用这种形式来计算其下标,如abc:97 * 10² + 98 * 10 + 99 = 10719。这样可以基本保证计算出的下标具有唯一性。
    但是如果存在yyyyyyyyyyyyyyyy这样的字符,计算出来的下标值显然很大,而实际情况中我们用不了这么大的数组去存放元素,创建如此大的数组也没有什么意义。

  • 总结
    对于以上两种方案,第一种计算出来的哈希值太小,容易出现重复;而第二种方案计算出来的哈希值又过于太大,浪费空间。第二种方案可以避免下标重复,但是计算的哈希值太大,这里我们可以用一种方式将这个哈希值压缩到当前数组的长度范围类。下面举个例子: 有5000个单词,我们通常会定义5000个长度的数组去存储,而在实际情况下,需要定义更大的空间来存储这些单词,因为不能保证每个单词在哈希化的时候都映射在不同的位置。假设我们定义一个两倍的大小空间10000来存储这5000个单词。显然abc计算出来的下标值已经大于10000了,那么我们要对10719压缩到0-10000这个范围,有一种简单的方式就是取余法,如:10719%10000=719,这使得10719被压缩到了这个数组的范围内。

尽管上面提到的方式能够有效的降低哈希值相同的概率,但是也无法避免会出现相同哈希值的情况(如某个单词的哈希值可能是20819),如果出现哈希值相同的情况,我们就将其称为哈希冲突。虽然我们希望冲突不发生,但在是实际情况中我们无法避免。既然冲突无法避免,我们只能去处理冲突。


4.什么是哈希冲突?

从上可知通过哈希函数计算出的索引,即使关键字不同,索引也会有可能相同,这就是哈希冲突。 影响Hash碰撞(冲突)发生的除了Hash函数本身意外,底层数组容量也是一个重要原因。很明显,极端情况下如果数组容量为1, 就必然发生碰撞,如果数组容量无限大,哪碰撞的概率非常之低。所以,哈希碰撞还取决于负载因子。负载因子是存储的键值对数目与数组容量的比值,比如数组容量100,当前存贮了90个键值对,负载因子为0.9。负载因子决定了哈希表什么时候扩容,如果负载因子的值太大,说明存储的键值对接近容量,增加碰撞的风险,如果值太小,则浪费空间。所以,既然冲突无法避免,就必须要有解决Hash冲突的机制方法。
解决Hash冲突的机制方法(主要有四类方法):

  • 开放定址法(常用)
  • 外部拉链法(常用)
  • 公共溢出区(不常用)
  • 再Hash法(不常用)


(1.)开放定址法(常用)

即当一个关键字和另一个关键字发生冲突时,使用某种探测技术在Hash表中形成一个探测序列,然后沿着这个探测序列依次查找下去,当碰到一个空的单元时,则插入其中。比较常用的探测方法有线性探测法,比如有一组关键字{12,13,25,23,38,34,6,84,91},Hash表长为14,Hash函数为address(key)=key%11,当插入12,13,25时可以直接插入,而当插入23时,地址1被占用了,因此沿着地址1依次往下探测(探测步长可以根据情况而定),直到探测到地址4,发现为空,则将23插入其中。


(2.)外部拉链法(常用)

  • 将一组数据存储到长度为13的链表中,在下标为1的位置我们发现有多个数字(1,14,27,79),这些数字通过取余法,计算出的下标都为1,这就产生了哈希冲突,因此我们只能将这些数据全部保存到这个位置,所以我们需要一个数组或者是链表来存储这些数据。
  • 在这里我们假设用链表来存储,一旦出现哈希冲突将数据放入到链表头部。
  • 当要查询时,我们先根据哈希化后计算出的下标值,拿到哈希表中对应的链表,再在链表中依次查询我们要查询的数据。


五. 树结构


1.树结构的定义

树被用来存储具有层级关系的数据,比如文件系统中的文件。树结构是一种非线性存储结构,存储的是具有“一对多”关系的数据元素的集合。

上图是使用树结构存储的集合 {A,B,C,D,E,F,G,H,I,J,K,L,M} 的示意图。对于数据 A 来说,和数据 B、C、D 有关系;对于数据 B 来说,和 E、F 有关系。这就是“一对多”的关系。 将具有“一对多”关系的集合中的数据元素按照上图的形式进行存储,整个存储形状在逻辑结构上看,类似于实际生活中倒着的树,所以称这种存储结构为“树型”存储结构。


2.树结构的名词解释

  • 子树: 除了根节点外,每个子节点都可以分为多个不相交的子树。
  • 孩子与双亲: 若一个结点有子树,那么该结点称为子树根的"双亲",子树的根是该结点的"孩子"。在图一中,B、H是A的孩子,A是B、H的双亲。
  • 兄弟: 具有相同双亲的节点互为兄弟,例如B与H互为兄弟。
  • 节点的度: 一个节点拥有子树的数目。例如A的度为2,B的度为1,C的度为3.
  • 叶子: 没有子树,也即是度为0的节点。
  • 分支节点: 除了叶子节点之外的节点,也即是度不为0的节点。
  • 内部节点: 除了根节点之外的分支节点。
  • 层次: 根节点为第一层,其余节点的层次等于其双亲节点的层次加1.
  • 树的高度: 也称为树的深度,树中节点的最大层次。
  • 有序树: 树中节点各子树之间的次序是重要的,不可以随意交换位置。
  • 无序树: 树种节点各子树之间的次序是不重要的。可以随意交换位置。
  • 森林: 0或多棵互不相交的树的集合。例如图二中的两棵树为森林。


3.树的种类


(1.)二叉树

⼆叉树,顾名思义,每个节点最多有两个 “叉”,也就是两个⼦节点,分别是左⼦节点和右⼦节点。不过,⼆叉树并不要求每个节点都有两个⼦节点,有的节点只有左⼦节点,有的节点只有右⼦节点。

例如,a就是一棵二叉树,而图 1b则不是。


(2.)满二叉树

满二叉树就是所有非叶子节点都有左右孩子,并且所有叶子节点都在同一层级上。二叉树除了叶结点外所有节点都有两个子节点。

简单来说:就是每一层都铺满,直到下一层没有任何元素。


(3.)完全二叉树

从根往下数,除了最下层外都是全满(都有两个子节点),而最下层所有叶结点都向左边靠拢填满。

简单来说:就是从根结点到倒数第二层满足满二叉树,最后一层可以不完全填充,其叶子结点都靠左对齐。


(4.)二叉查找树

二叉查找树(BST:Binary Search Tree),也称二叉搜索树、有序二叉树(ordered binary tree),排序二叉树(orted binary tree)是一种特殊的二叉树,它改善了二叉树节点查找的效率。

二叉查找树有以下性质:

  • 若左子树不空,则左子树上所有节点的值均小于它的根节点的值。
  • 若右子树不空,则右子树上所有节点的值均大于它的根节点的值。
  • 左、右子树也分别为二叉排序树。
  • 没有键值相等的节点。


(5.)平衡二叉树(AVLTree)

二叉查找树可以任意地构造,同样是6,8,10,11,13,15,17这七个数字,也可以按照下图的方式来构造:

由于普通的二叉查找树会容易失去”平衡“,极端情况下,二叉查找树会退化成线性的链表,导致插入和查找的复杂度下降到 O(n)如上图。因此若想二叉树的查询效率尽可能高,需要这棵二叉树是平衡的,从而引出新的定义——平衡二叉树,或称AVL树。
平衡因子
平衡二叉树上的结点左子树的深度减去右子树的深度的值成为该结点的平衡因子,只有当二叉树所有结点的平衡因子都是-1, 0, 1这三个值的时候,这颗二叉树才是一颗合格的AVL树。
如下图就是一颗典型的AVL树,每个节点旁边都标注了平衡因子:

  • 其中结点4的左子树高度是1,右子树不存在,所以该结点的平衡因子是1-0=1。
  • 结点7的左子树不存在,右子树高度是1,所以平衡因子是0-1=-1。
  • 所有的叶子结点,不存在左右子树,所以平衡因子都是0。

AVL树是带平衡条件的二叉查找树,一般是用平衡因子差值决定并通过旋转来实现,左右子树树高差不超过1,平衡条件非常严格(树高差只有1),只要插入或删除不满足上面的条件就要通过旋转来保持平衡。由于旋转是非常耗费时间的。我们可以推出AVL树适合用于插入删除次数比较少,但查找多的情况。因为AVL是一种高度平衡的二叉树,所以通常的结果是,维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常严格整体平衡的红黑树。当然,如果场景中对插入删除不频繁,只是对查找特别有要求,AVL还是优于红黑的。 Windows对进程地址空间的管理用到了AVL树。


(6.)红黑树(RBTree)

红黑树属于平衡二叉树,是不太严格的平衡二叉树。主要是通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,确保没有一条路径会比其他路径长2倍,因而是近似平衡的。所以相对于严格要求平衡的AVL树来说,它的旋转保持平衡次数较少。用于搜索时,插入删除次数多的情况下就用红黑树来取代AVL。
红黑树树有以下性质:

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

对于一棵红黑树的操作最基本的无外乎增删改查,其中查和改都不会改变树的结构,所以与普通平衡二叉树操作无异。剩下的就是增删操作,插入和删除都会有可能破坏树的结构,不过借助一定的平衡策略能够让树重新满足定义。平衡策略可以简单概括为三种:左旋转、右旋转,以及 变色。在插入或删除结点之后,只要我们沿着结点到根的路径上执行这三种操作,就可以最终让树重新满足红黑树的定义。

参考资料:
数据结构中的树存储结构
3 分钟理解完全二叉树、平衡二叉树、二叉查找树
树 - 基础和Overview

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值