数据结构基础知识
数据结构
数据结构指数据的存储、组织方式。
数据结构 | 优点 | 缺点 |
---|---|---|
栈 | 顶部元素插入和取出快 | 除顶部元素外,存取其他元素都很慢 |
队列 | 顶部元素取出和尾部元素插入快 | 存取其他元素都很慢 |
链表 | 插入和删除都快 | 查找慢 |
二叉树 | 插入、删除、查找都快 | 删除算法复杂 |
红黑树 | 插入、删除、查找都快 | 算法复杂 |
散列表 | 插入、删除、查找都快 | 数据散列,对存储空间有浪费 |
位图 | 节省存储空间 | 不方便描述复杂的数据关系 |
1、栈
栈(Stack)又叫堆栈,是允许在同一端进行插入和删除操作的特殊线性表。允许插入和删除操作的一端叫做栈顶(Top),另一端叫做栈底(Bottom),栈底固定,栈顶浮动。栈中元素个数为0时,该栈叫做空栈。插入的过程叫做入栈(Push),删除的过程叫做出栈(Pop)。
栈也叫做先进后出(FILO,First In Last Out)的线性表。
要实现一个栈,需要先实现以下核心方法:
(1)push(E e):向栈中压入一个数据,先入栈的数据在最下边
(2)pop():弹出栈顶元素,即移除栈顶元素并返回
(3)peek():查找栈顶元素但不移除
2、队列
队列是一种只允许在前端进行删除操作且在表的后端进行插入操作的线性表。执行插入操作的一端叫做队尾,执行删除操作的一端叫做队头。没有元素的队列叫做空队列,在队尾插入一个元素叫做入队,从队头删除一个元素叫做出队。
队列也叫做先进先出(FIFO,First In First Out)的线性表。
要实现一个队列,需要先实现以下核心方法:
(1)add():向对尾加入一个元素
(2)poll():删除队头元素并返回
(3)peek():查找栈顶元素但不移除
3、链表
链表是由一系列节点(链表中的每一个元素都叫做一个元素)组成的数据结构,节点可以在运行过程中动态生成。每个节点都包含两部分内容:存储数据的数据域;存储下一个节点地址的指针域。
由于链表是随机存储数据的,因此在链表中插入数据的时间复杂度为O(1),但是在链表中查找一个节点时需要遍历链表中所有元素,因此时间复杂度为O(n)。链表结构的优点是插入快,缺点是查询慢。
链表有3种不同的类型:单向链表、双向链表及循环链表。
3.1、单向链表
单向链表(又称单链表)是链表的一种,其特点是链表的链接方向是单向的,访问链表时要从头部开始顺序读取。
3.2、双向链表
在双向链表的每个数据节点中都有两个指针Next和Prev,分别指向其直接后继和直接前驱节点。所以,从双向链表中的任意一个节点开始,都可以很方便地访问它的前驱节点和后继节点。
3.3、循环链表
循环链表的链式存储结构的特点是:表中最后一个节点的指针域指向头节点,整个链表形成一个环。
循环链表的实现和单项链表十分相似,只是在链表中,尾部元素的Next指针不再是null,而是指向头节点。
4、散列表
散列表(Hash Table,也叫作哈希表)是根据数据的关键码值(Key-Value对)对数据进行存取的数据结构。散列表通过映射函数把关键码值映射到表中的一个位置来加快查找。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
在散列表M中,给定任意的关键字Key,代入散列函数hash(key)中,得到包含该关键字的记录在表M中的位置。
散列表算法通过在数据元素的存储位置和它的关键字key之间建立一个确定的对应关系,使每个关键字和散列表中唯一的存储位置相对应。在查找时只需要根据这个对应关系找到给定关键字在散列表的位置即可,真正做到一次查找命中。
Hash的应用
Hash主要用于信息安全加密和快速查询的应用场景。
- 信息安全:Hash主要被用于信息安全领域的加密算法中,它把一些不同长度的信息转化成杂乱的128位编码,这些编码的值叫做Hash值。也可以说,Hash就是为数据生成指纹特征。
- 快速查找:散列表,又叫做哈希表,是一种更加快捷的查找技术。基于列表集合查找的一般做法是对数据进行逐个比对,若比对不成功,则缩小查找范围,直到查找成功。而散列表则是先通过hash()和key值来确定存储位置,即先缩小查找范围,再进行查找。
5、二叉排序树
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree)或二叉搜索树。二叉排序树满足以下条件:
(1)若左子树不为空,则左子树上的所有节点的值均小于它的根节点的值;
(2)若右子树不为空,则右子树上的所有节点的值均大于或等于它的根节点的值;
(3)左、右子树也分别为二叉排序树。
5.1、插入操作
在二叉排序树中进行插入操作只需找到待插入的父节点,将数据插入即可。具体流程如下:
(1)若当前节点为空,表名该二叉树为空树,则将待插入节点插入到根节点位置;
(2)将待插入节点与当前节点进行比较,如果待插入节点的值小于当前节点的值,则在当前节点的的左子树中查找,直到左子树为空,则当前节点为要找的父节点,将新节点插入到当前节点的左子树即可。
(3)将待插入节点与当前节点进行比较,如果待插入节点的值大于等于当前节点的值,则在当前节点的的右子树中查找,直到右子树为空,则当前节点为要找的父节点,将新节点插入到当前节点的右子树即可。
注意:二叉排序树中能否出现值相等的节点,取决于使用时的需求,这里默认是将值相等的节点视作应当保留且放置到右子树上。
5.2、删除操作
二叉排序树(BST)的删除分三种情况:
(1)待删除的节点没有子节点:直接删除该节点
(2)待删除的节点有一个子节点:使用子节点替换待删除节点
(3)待删除的节点有两个子节点:使用其左子节点或者右子节点替换待删除的节点
5.3、查找操作(判断树中有没有当前节点)
二叉排序树的查找方式和效率接近二分查找法,因此此可以很容易获取最大值(最右最深子节点)和最小值(最左最深子节点)。
具体的查找流程为:将要查找的数据与根节点的值进行比较,如果相等就返回,如果小于根节点的值,就到左子树中递归查找;如果大于根节点的值,就到右子树中递归查找。
6、平衡二叉树
平衡二叉树是为了解决二叉查找树退化成链表的情况,而红黑树是为了解决平衡二叉树在插入、删除等操作需要频繁调整的情况。
平衡二叉树右如下特点:
(1)具有二叉排序树的全部特性;
(2)每个节点的左子树和右子树的高度差至多等于1。
在插入过程中,会出现以下四种情况破坏平衡二叉树(AVL)的特性,可以采取如下相应的旋转。
1、左-左型:做右旋
2、右-右型:做左旋
3、左-右型:先做左旋,后做右旋,如下图所示
4、右-左型:先做右旋,在做左旋
6、红黑树
红黑树(Red-Black Tree,R-B Tree)是一种自平衡的二叉查找树。在红黑树的每个节点上都多出一个存储位表示节点的颜色,颜色只能是红或者黑。
6.1、红黑树的特点
(1)每个节点要么是黑色,要么是红色
(2)根节点是黑色
(3)每个叶子节点是黑色【注意:叶子节点,指为空(NIL或NULL)的叶子节点】
(4)如果一个节点是红色的,则它的子节点必须是黑色的
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点
6.2、红黑树的左旋
对节点X进行左旋,指将节点X的右子节点Y设为节点X的父节点,即将节点X变成节点Y的左子节点,节点Y原来的左子节点变为节点X的右子节点。如下图所示:
6.3、红黑树的右旋
对节点Y进行右旋,指将节点Y的左子节点X设置为节点Y的父节点,即将节点Y变为节点X的右子节点,节点X原来的右子节点变为节点Y的左子节点。如下图所示:
6.4、红黑树的添加
红黑树的添加分为3步:
(1)将红黑树看作一棵二叉查找树,并以二叉查找树的插入规则插入新节点;
(2)将插入的节点涂为“红色”或者“黑色”;
(3)通过左旋、右旋或着色操作。使之重新成为一棵红黑树。
依据被插入的节点的父节点的情况,可以将具体的插入分为3种情况来处理:
(1)如果被插入的节点是根节点,则直接将此节点涂为黑色;
(2)如果被插入的节点是黑色的,则什么也不需要做,在节点插入后,任然是红黑树;
(3)如果被插入的节点的父节点是红色的,则被插入节点的父节点一定存在非空祖父节点,即被插入节点也一定存在叔叔节点,即使叔叔节点(叔叔节点指的是当前节点的父节点的兄弟节点或者说是当前节点的祖父节点的另一个子节点)为空,我们视之为存在,空节点本身为黑色节点。然后根据叔叔节点的颜色,在被插入节点的父节点是红色时,进一步分3种情况来处理:
- 1、如果当前节点的父节点时红色的,当前节点的叔叔节点是红色的,则将该节点设为黑色的,将叔叔节点设为黑色的,将祖父节点设为红色的,将祖父节点设为当前节点。
- 2、如果当前节点的父节点是红色的,当前节点的叔叔节点是黑色的且当前节点是右节点,则将父节点设为当前节点,以新节点为支点左旋。
- 3、如果当前节点的父节点是红色的,当前节点的叔叔节点是黑色的且当前节点是左节点,则将父节点设为黑色的,将祖父节点设为红色的,以祖父节点为支点右旋
6.5、红黑树的删除
、、、
7、图
图是由有穷非空集合的顶点和顶点之间的边组成的集合,通常表示为G(V,E)。其中G表示一个图,V是图G中顶点的集合,E是图中边的集合。
在线性结构中,每个元素都只有一个直接前驱和直接后继,主要用来表示一对一的数据结构;在树型结构中,数据之间有着明显的父子关系,每个数据和其子节点的多个数据相关,主要用来表示一对多的数据结构;在图形结构中,数据之间具有任意关系,图中任意两个数据之间都可能相关,可用来表示多对多的数据结构。
7.1、无向图和有向图
若从顶点A到顶点B的边没有方向,则称这条边为无向边。由顶点和无向边组成的图叫做无向图,用无序对(A,B)来表示无向边。
若从顶点C到顶点D的边有方向,则称这条边为有向边,也叫做弧。用有序偶<C,D>来表示有向边,C表示弧尾,D表示弧头。由顶点和有向边组成的图叫做有向图。
7.2、图的存储结构:邻接矩阵
图的邻接矩阵的存储方式是基于两个数组来表示图的数据结构并存储图中的数据。一个一维数组存储图中的顶点信息,一个二维数组(也叫做邻接矩阵)存储图中的边或弧的信息。设图G中有n个顶点,则邻接矩阵是一个n*n的方阵。
7.2.1、无向图的邻接矩阵
在无向图的邻接矩阵中,如果顶点A和顶点B相通,则用1来表示,如果不相同则用0来表示。
7.2.2、有向图的邻接矩阵
在有向图的邻接矩阵中,如果存在<A,B>这条有向边,则用1来表示,否则用0来表示。
7.2.3、带权重图的邻接矩阵
有些图的每条边上都带有权重,如果要将这些权值保存下来,则可以采用权值代替矩阵中的0、1,在权值不存在的元素之间用∞(无穷)表示。
7.3、图的存储结构:邻接表
数组和链表相结合的存储方式叫做邻接表。邻接表是图的一种链式存储结构,主要用于解决邻接矩阵中顶点多而边少时存在空间浪费的问题。
顶点和边的存储方式如下:
(1)将图中的顶点信息存储在一个一维数组中,同时在顶点信息中存储用于指向第1个邻接点的指针,以便查找该顶点的边信息。
(2)图中每个顶点V的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单向链表存储,如果是无向图,则称该链表是顶点V的边表,如果是有向图,则称链表是以顶点V为弧尾的出边表。
对于带权值的图,在节点定义中再增加一个权重值Weight的数据域,存储权值信息即可。
7.4、图的遍历
图的遍历指从图中某一顶点出发遍历图中每个顶点,且使每一个顶点仅被访问一次。图的遍历分为广度优先遍历和深度优先遍历,且对无向图和有向图都适用。
7.4.1、广度优先遍历
图的广度优先遍历类似于树的层次遍历。
其思想为:假设从图中某个顶点V出发,在访问了V后依次访问V的各个未曾访问过的邻接点,然后从这些邻接点出发依次访问它们的邻接点,并使先被访问节点的邻接点先于后被访问节点的邻接点被访问,直到图中所有已被访问的顶点的邻接点都被访问;若此时图中尚有顶点未被访问,则另选图中未曾被访问的一个顶点作为起始点重复上述过程,直到图中所有顶点均被访问。
7.4.2、深度优先遍历
图的深度优先遍历类似于树的先序遍历。
其思想为:从图的某个顶点V出发,在访问节点V后,依次从V未被访问的邻接点出发以深度优先的原则遍历图,直到图中所有和V节点路径连通的顶点都被访问;若此时图中尚有顶点未被访问,则另一个未曾访问的顶点作为起始点重复上述操作,直至图中所有节点都被访问。
8、位图
位图(Bitmap)通常基于数组实现,可以将数组中的每个元素都看作一系列二进制数,所有元素一起组成更大的二进制集合,这样可以大大节省空间。位图通常是用来判断某个数据不存在的,常用于Bloom Filter中判断数据是否存在,还可用于无重复数的排序等,在大鼠行业中使用广泛。
8.1、位图的数据结构
位图在内部维持一个M*N维的字符数组,在这个数组里面每个字节占8位,因此可以存储 MxNx8个数据。假如要存储的数据范围为0 ~ 15,则只需要使用M=1,N=2的数组进行存储,具体的数据结构如下所示:
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
若要存储的数据为{1,3,6,10,15},只需将有数据的位设置为1,表示该位存在数据,将其他位设置为0,如下所示:
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 |
8.2、位图的Java实现
在Java中使用byte[]字节数组来存储bit,1Byte=8bit。对于bit中的第i位,该为1则表示数据存在,为0则表示数据不存在。
8.3、布隆过滤器(Bloom Filter)
布隆过滤器用来判断某个元素(key)是否在某个集合中,该算法不需要存储key的值,对于每个key,只需要k个比特位,每个存储一个标志,用来判断key是否在集合中。
算法:
1、首先需要k个hash函数,每个函数可以把key散列成一个整数;
2、初始化时,需要一个长度为N比特的数组,每个比特位初始化为0;
3、某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位设置为1
4、判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。
优点:不需要存储key值,节省空间
缺点:无法删除;随着存入的元素数量增加,误算率随之增加。