一、数据结构概述
1.1 概念
在计算机科学中,数据结构是一种数据组织、管理和存储的格式 。它是相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术相关。
1.1.1 数据逻辑结构
数据的逻辑结构是指数据元素之间存在的逻辑关系,由数据元素的集合和定义在此集合上的关系组成。数据的逻辑结构与数据的存储无关,独立于计算机,是从具体问题抽象出来的数学模型。
数据的逻辑结构由两个要素构成,分别是:数据元素的集合和关系的集合 。
1.1.2 数据存储结构
数据的逻辑结构在计算机中的存储表示或实现叫做数据的存储结构,也叫物理结构。数据的存储结构依赖于计算机。
一般来说,一种数据结构的逻辑结构根据需要可以表示成多种存储结构,常用的存储结构有顺序存储、链式存储、索引存储和哈希存储等。
数据的顺序存储结构的特点是:借助元素在存储器中的相对位置来表示数据元素之间的逻辑关系;非顺序存储的特点是:借助指示元素存储地址的指针表示数据元素之间的逻辑关系。
1.1.3 数据的运算
数据操作是指对数据结构中的数据元素进行运算或处理。数据操作定义在数据的逻辑结构上,每种逻辑结构都需要一组对其数据元素进行处理以实现特定功能的操作,如插入、删除、更新等。数据操作的实现依赖于数据的存储结构。
数据的运算包括:检索、排序、插入、删除、修改等。
二、常见的数据结构
常见的数据结构有:数组(Array)、栈(Stack)、队列(Queue)、链表(Linked List)、树(Tree)、图(Graph)、堆(Heap)、散列表(Hash)。
2.1 数组(Array)
数组是一种线性结构,是可以在内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从0开始。
- 优点:访问数据简单。
- 缺点:添加和删除数据比较耗时间,因为要移动其他的元素;数组只能存储一种类型的数据;数组的大小固定后就无法扩容了
- 使用场景:频繁查询,对存储空间要求不大,很少增加和删除的情况。
int[] a = {1,3,4,5,6,8,9,26,30,89};//直接创建并声明容量元素的数组 int[] data = new int[100];// 创建一个整型int数组,大小是100个 data[0] = 1; // 向数组第一个元素赋值1; data[1] = 2; // 向数组第二个元素赋值2;
JDK提供的顺序表有:java.util.ArrayList 其底层实现就是数组
数组(顺序表)时间复杂度分析:
- 查询get(i) ,不难看出不论数据元素量N有多大,只需要一次eles[i] 就可以获取到对应的元素,所以时间复杂度为O(1)
- 插入insert(int i, T t),每一次插入,都需要把i位置后面的元素移动一次,随着元素数量N的增大,移动的元素也越多,时间复杂度为O(n)
- 删除元素remove(int i),每一次删除,都需要把i位置后面的元素移动一次,随着数据量N的增大,移动的元素也越多,时间复杂度为O(n)
- 数组长度是固定的,所以在操作的过程中涉及到了容器扩容操作。这样会导致顺序表在使用过程中的时间复杂度不是线性的,在某些扩容的结点处,耗时会突增,尤其是元素越多,这个问题越明显
2.2 链表(Linked List)
链表是一种数据元素按照链式存储结构进行存储的数据结构,这种存储结构具有在物理上非连续、非顺序的特点。链表由一系列数据结点构成,每个数据结点包括数据域和指针域两部分。其中,指针域保存了数据结构中下一个元素存放的地址。
链表结构中数据元素的逻辑顺序通过链表中的指针链接次序实现 。
根据指针的指向,链表能形成不同的结构,例如单链表,双向链表,循环链表等。
链表时间复杂度分析:
- get(int i):每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素N的增多,比较的元素越多,时间复杂度为O(n)
- insert(int i, T t):每一次插入,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n)
- remove(int i):每一次移除,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n)
ArrayList vs LinkedList
ArrayList
- 基于数组,需要连续内存
- 随机访问快(指根据下标访问)
- 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低
- 可以利用 cpu 缓存,局部性原理
LinkedList
- 基于双向链表,无需连续内存
- 随机访问慢(要沿着链表遍历)
- 头尾插入删除性能高
- 占用内存多
链表的优点:
- 链表是很常用的一种数据结构,不需要初始化容量,可以任意加减元素;
- 添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快;
缺点:
- 因为含有大量的指针域,占用空间较大;
- 查找元素需要遍历链表来查找,非常耗时。
JDK提供的链表有:java.util.LinkedList
适用场景:
- 数据量较小,需要频繁增加,删除操作的场景;
- 快慢指针:求中间值问题、单向链表是否有环问题、有环链表入口问题;
- 循环链表:约瑟夫问题
2.3 栈(Stack)
栈是一种特殊的线性表,又称为栈。是一种基于先进后出(FILO)的数据结构,它只能在表的固定端进行数据结点的插入和删除操作。栈按照先进后出、后进先出的原则来存储数据,也就是说,先插入的数据将被压入栈底,最后插入的数据在栈顶,读出数据时,从栈顶开始逐个读出。
栈在汇编语言程序中,经常用于重要数据的现场保护。栈中没有数据时,称为空栈 。
我们称数据进入到栈的动作为压栈,数据从栈中出去的动作为弹栈。
JDK提供的栈有:java.util.Stack
应用场景:括号匹配问题;逆波兰表达式求值问题;实现递归功能方面的场景,例如斐波那契数列。
2.4 队列(Queue)
队列是一种基于先进先出(FIFO)的数据结构,是一种只能在一端进行插入,在另一端进行删除操作的特殊线性表,它按照先进先出的原则存储数据,先进入的数据,在读取数据时先被读取出来。
入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头
JDK提供的队列接口有:java.util.Queue
使用场景:因为队列先进先出的特点,在多线程阻塞队列管理中非常适用。
2.5 树(Tree)
树是典型的非线性结构,它是由n(n>0)个有限结点组成的一个具有层次关系的集合。在树结构中,有且仅有一个根结点,该结点没有前驱结点。在树结构中的其他结点都有且仅有一个前驱结点。
树具有以下特点:
- 每个结点有零个或多个子结点;
- 没有父结点的结点为根结点;
- 每一个非根结点只有一个父结点;
- 除了根节点外,每个子节点可以分为多个不相交的子树;
在日常的应用中,我们讨论和用的更多的是树的其中一种结构,就是二叉树、平衡树、红黑树、B树、B+树。
应用场景:
- JDK1.8中 HashMap的底层源码中用到了数组+链表+红黑树;
- 磁盘文件中使用B树做为数据组织,B树大大提高了IO的操作效率;
- mysql数据库索引结构采用B+树;
2.5.1 二叉树:满二叉树和完全二叉树
二叉树是一种比较有用的折中方案,它添加,删除元素都很快,并且在查找方面也有很多的算法优化,所以,二叉树既有链表的好处,也有数组的好处,是两者的优化方案,在处理大批量的动态数据方面非常有用。
二叉树有很多扩展的数据结构,包括平衡二叉树、红黑树、B+树等,这些数据结构在二叉树的基础上衍生了很多的功能,在实际应用中广泛用到,例如mysql的数据库索引结构用的就是B+树,还有HashMap的底层源码中用到了红黑树。
二叉树是树的特殊一种,具有如下特点:
- 每个结点最多有两颗子树,结点的度最大为2。
- 左子树和右子树是有顺序的,次序不能颠倒。
- 即使某结点只有一个子树,也要区分左右子树。
顺序结构(数组来存储,heap里面讲)
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
链式结构
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程学到高阶数据 结构如红黑树等会用到三叉链。
链式结构接口如下:(包括三种遍历方式:前序遍历、中序遍历、后序遍历,以及层序遍历)
2.6 散列表(Hash)
散列表,也叫哈希表,是根据关键码和值 (key和value) 直接进行访问的数据结构,通过key和value来映射到集合中的一个位置,这样就可以很快找到集合中的对应元素。
记录的存储位置=f(key)
这里的对应关系 f 成为散列函数,又称为哈希 (hash函数),而散列表就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里,这种存储空间可以充分利用数组的查找优势来查找元素,所以查找的速度很快。
在存储数据的过程中,如果发生冲突,可以利用链表在已有数据的后面插入新数据来解决冲突。这种方法被称为“链地址法”。除了链地址法以外,还有几种解决冲突的方法。其中,应用较为广泛的是“开放地址法”。
DK提供的哈希表有:java.util.HashMap
散列表应用场景:
- 哈希表的应用场景很多,当然也有很多问题要考虑,比如哈希冲突的问题,如果处理的不好会浪费大量的时间,导致应用崩溃。
- 解决哈希冲突问题:1 可以对数组扩容; 2 优化hash计算方式;
2.7 堆(Heap)
堆是一种特殊的树形数据结构,一般讨论的堆都是二叉堆。堆的特点是根结点的值是所有结点中最小的或者最大的,并且根结点的两个子树也是一个堆结构 。
被用于实现“优先队列”(priority queues)。优先队列是一种数据结构,可以自由添加数据,但取出数据时要从最小值开始按顺序取出。在堆的树形结构中,各个顶点被称为“结点”(node),数据就存储在这些结点中。堆有下列特点:
- 每个节点最多有两个子节点
- 排列顺序必须从上到下,同一行从左到右
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 存放数据时,一般会把新数据放在最下面一行靠左的位置。如果最下面一行没有多余空间时,就再往下另起一行,并把数据添加到这一行的最左端。
堆的性质:
- 堆是一个完全二叉树
- 堆中某个结点的值总是不大于或不小于其父结点的值;
- 除了树的最后一层结点不需要是满的,其它的每一层从左到右都是满的,如果最后一层结点不是满的,那么要求左满右不满;
- 它通常用数组来实现;
将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、 斐波那契堆等。 一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号, 如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树 称为完全二叉树。 一棵深度为k且有2^k−1个结点的二叉树称为满二叉树。也就是说除了叶子节点都有2个子节点,且 所有的叶子节点都在同一层。
堆应用场景:因为堆有序的特点,一般用来做数组中的排序,称为堆排序。
2.8 图(Graph)
图是另一种非线性数据结构。图的数据结构包含一个有限的集合作为结点集合,以及一个无序对(对应无向图)或有序对(对应有向图)的集合作为边的集合。如果两个结点之间存在一条边,那么就表示这两个结点具有相邻关系 。
无向图:
有向图:
图的搜索:
- 深度优先搜索:指的是在搜索时,如果遇到一个结点既有子结点,又有兄弟结点,那么先找子结点,然后找兄弟结点。
- 广度优先搜索:指的是在搜索时,如果遇到一个结点既有子结点,又有兄弟结点,那么先找兄弟结点,然后找子结点。
图的应用场景:
- 道路畅通工程
- 最短路径