数据结构:存储数据的结构
算法:操作数据的方法 如何操作数据效率高,更节约资源
数据结构
可分为 线性结构、树形结构、图
线性表
数据像线一样排列 主要有:数组、链表、队列、栈等
数组
线性表结构,用一组连续的内存空间来存储相同类型的数据
如 [1,2,3,4,5,6,7,8,9…,n]
逻辑结构:数组a有n个元素,可表示为(a1,a2,a3..an)
物理结构:在连续的内存空间存储数组,设置每个元素的下标进行查找
数据访问:
a[i] = bassAddress + i * dataTypeSize (基础数据地址+下标*数据类型大小 即int型为4个字节)
下标从0开始,因为C设计从0开始的,如果从1开始计算机会多走一次i-1的减法
效率:因为存在下标,访问数据就会高效
数据插入与删除:
每次插入与删除都会造成数据空间的下标改变,挪动数据,于是操作效率就变得低效
算法提升 如需要删除多个数据时,若能够一次执行批量删除,就只需要挪动一次数据。
依次记录删除的数据不进行删除挪动,记录完毕后再进行统计操作挪动数据
如JVM标记回收垃圾清除算法
容器类 java ArrayList c++ Vector
ArrayList 底层由数组实现,动态扩容,提供相关操作进行对数组的获取、新增、删除...
源码分析 : 第一次add时,数组初始空间大小为10,add元素超过原数组空间,会开辟一个1.5倍大小的新数组,将新数组拷贝到原数组
链表
非连续、非顺序的存储结构,每个结点存在 一个数据域与指向下一结点的指针域
单链表 : 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> … -> n
结点 : 一个数据域与指向下一结点的指针域 当后续无结点时,指针域存 null
类型 : 单链表、循环链表、双链表(存储数据域、前结点地址、后结点地址)、循环双链表
性能 : 大小没有限制,天然支持扩容,但是随机访问时没有下标的存在只能遍历整个链表进行查询
容器类: java LinkedList
源码分析: 双向链表 E item 元素;LinkedList.Node<E> next 后继;LinkedList.Node<E> prev 前驱;
add(),将新增元素添加至最后,创建结点node(last,element,null) 前驱为前一结点,后继为null,
并设置此结点为尾结点last,若前驱也为null时,即头结点first也是该新增结点
get(index),通过判断 index < (size>>1) ,若获取索引小于链表中间值,从first往后查询后继next;
若大于等于中间值时,从last往前查询前驱prev
与ArrayList的区别:
区别 | ArrayList | LinkedList |
---|---|---|
底层实现 | 数组 | 双向链表 |
随机访问 | 基于下标计算,可快速访问,效率高 | 根据头或尾结点依次根据地址指针进行查询,效率低 |
插入删除 | 数据需要保持连续性,插入删除时需要挪动数据,并且在内存空间不够时会进行扩容拷贝数组,效率低 | 对大小无限制,新增删除时仅需要改变前驱后继的指针地址,效率高 |
内存占用 | 仅需保存数据,较小 | 不只保存数据同时需要保持前驱后继两个地址指针,占用空间较大 |
//反转链表
class node{
int val;
node next;
node(val){this.val=val;}
}
public static node resveLinke(node head){
//定义前结点
node prev = null;
//获取临时结点
node temp = head;
while(temp!=null){
node cuur = temp.next;
temp.next = prev;
prev = temp;
temp = cuur;
}
return prev;
}
栈
特点:先进后出,后进先出 LIFO-“LAST IN FIRST OUT” 一种受限制的线性表
只能在栈两端进行操作数据,数据插入、删除称为 入栈、出栈
栈顶 <–>1,2 ,3,4,5,6,…,n]栈底
实现:
基于数组的顺序栈,基于链表的链式栈
Stack源码分析:
容器类
class Stack<T> {
//常用方法
长度
int size();
向栈顶添加元素
void push(T t);
移除顶部元素
T pop();
查看顶部元素
T peek();
}
队列
特点:先进先出,后进后出 FIFO-“FIRST IN FIRST OUT” 一种受限制的线性表
数据从尾部进,从头部出,入列、出列
实现:
基于数组的顺序列,单链表的链式列
应用:线程池、缓存、并发访问
容器类: Queue,LinkedList
入列-> n , … , 5 , 4 , 3 , 2 , 1 -> 出列
散列表 (哈希表)
特点: 由 key-value(键值对)的结构组成的集合
一般 key 由一个数组定义 value由一个链表、
哈希表中,key存入的是hash值,当hash冲突时,会去比对key是否相等,
如果做插入操作时 hash 相等 ,key 不等 ,会直接添加在原有值链后
hash 相等 ,key 相等,会直接替换原有值;
//容器类
public class HashMap<K,V> {
//链节点储存表
transient Node<K,V>[] table;
//长度
transient int size;
//单链表
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//hash值
final K key;//主键
V value;//值
Node<K,V> next;//下一节点
}
//hash函数
static final int hash(Object key) {
int h;
//null 为 0 , h 取key的hashcode并做 位运算 ^(异或运算) 取相同位相等则结果为0,否则为1 >>>16 右移16位
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//获取值
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//放入值
public V put(K key, V value) {
/**
* 这是map具体实现put的方法
*
* @param hash 根据key计算的hash
* @param key 键
* @param value 放入的值
* @param onlyIfAbsent 如果为true,则不可修改替换原有值
* @param evict 如果为false,则为创造模式put
* @return previous value, or null if none
*/
return putVal(hash(key), key, value, false, true);
}
}
//Set底层维护了一个Map,它将原始以Key存入Map中,实现数据无重复
树
树是一种数据结构,它是n(n>=0)个节点的有限集。n=0时称为空树。n>0时,有限集的元素构成一个具有层次感的数据结构。
名词结构
子树
: 除了根节点外,每个子节点都可以分为多个不相交的子树。孩子与双亲
: 若一个结点有子树,那么该结点称为子树根的"双亲",子树的根是该结点的"孩子"。兄弟
: 具有相同双亲的节点互为兄弟。节点的度
: 一个节点拥有子树的数目。叶子
: 没有子树,也即是度为0的节点。分支节点
: 除了叶子节点之外的节点,也即是度不为0的节点。内部节点
: 除了根节点之外的分支节点。层次
: 根节点为第一层,其余节点的层次等于其双亲节点的层次加1.树的高度
: 也称为树的深度,树中节点的最大层次。有序树
: 树中节点各子树之间的次序是重要的,不可以随意交换位置。无序树
: 树种节点各子树之间的次序是不重要的。可以随意交换位置。森林
: 0或多棵互不相交的树的集合。
二叉树
最多有两棵子树的树被称为二叉树
- 斜树: 所有节点都只有左子树的二叉树叫做左斜树,所有节点都只有右子树的二叉树叫做右斜树。(本质就是链表)
- 满二叉树: 二叉树中所有非叶子结点的度都是2,且叶子结点都在同一层次上
- 完全二叉树: 如果一个二叉树与满二叉树前m个节点的结构相同,这样的二叉树被称为完全二叉树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zMFpyj4P-1609394285764)(D:\documents\study_note\算法与数据结构\二叉树.png)]
遍历算法
-
前序遍历 根-左-右
public void preOrder(TreeNode node){ if(node != null){ //TODO System.out.println(node.val); preOrder(node.left); preOrder(node.right); } }
-
中序遍历 左-根-右
public void inOrder(TreeNode node){ if(node != null){ inOrder(node.left); //TODO System.out.println(node.val); inOrder(node.right); } }
-
后序遍历 左-右-根
public void postOrder(TreeNode node){ if(node != null){ postOrder(node.left); postOrder(node.right); //TODO System.out.println(node.val); } }
二叉查找树 BST (Binary Search Tree)
- 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
- 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
- 任意节点的左、右子树也分别为二叉查找树;
- 没有键值相等的节点。
二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低为 O ( log n ) 。二叉查找树是基础性数据结构,用于构建更为抽象的数据结构,如集合、多重集、关联数组等。
二叉平衡树 AVL
左右子树深度不超过1
要么是棵空树,要么其根节点左右子树的深度之差的绝对值不超过1;
其左右子树也都是平衡二叉树;
二叉树节点的平衡因子定义为该节点的左子树的深度减去右子树的深度。则平衡二叉树的所有节点的平衡因子只可能是-1,0,1。
红黑树 RBT(Red-Black Tree)
红黑树是一棵二叉搜索树,它在每个节点增加了一个存储位记录节点的颜色,可以是RED,也可以是BLACK;通过任意一条从根到叶子简单路径上颜色的约束,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡。
- 每个结点要么是红的要么是黑的。(红或黑)
- 根结点是黑的。 (根黑)
- 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。 (叶黑)
- 如果一个结点是红的,那么它的两个儿子都是黑的。 (红子黑)
- 对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。(路径下黑相同)
- 添加一个节点时默认其为红节点,如果其父为黑节点则直接添加,若为红,调整改子树黑节点数
容器类: ConcurrentHashMap & TreeMap
HashMap的key冲突节点超过8时,链表->红黑树,小于8时,红黑树->链表,put源码解析
class HashMap{
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
*实现Map.put及相关方法
*
* @param hash 键的hash值
* @param key 键
* @param value 值
* @param onlyIfAbsent如果为true,请不要更改现有值
* @param evict,如果为false,则表处于创建模式。
* @返回上一个值,如果没有则返回null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//扩容后的长度 初始key数组 为16
if ((p = tab[i = (n - 1) & hash]) == null) //hash key 不冲突
tab[i] = newNode(hash, key, value, null); //在末位添加节点
else {// hash key 冲突时
Node<K,V> e; K k;
// key 值相等时
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//获取替换值
// key值不等
else if (p instanceof TreeNode)//判断当前值是否为红黑树存储
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//调用树添加节点
else {//否则为链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//当前节点为末节点 null 时
p.next = newNode(hash, key, value, null);//增加在末位
if (binCount >= TREEIFY_THRESHOLD - 1) //当插入第8个节点时,
treeifyBin(tab, hash);//转换成红黑树,同时扩展key数组长度
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;//当前节点等于目标节点时,直接结束
p = e;
}
}
if (e != null) { //e != null。存在原有值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)//为修改模式或原值为null
e.value = value;//设置为新值
afterNodeAccess(e);//空函数,为LinkedHashMap后处理的回调
return oldValue;//返回原值
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);//空函数,为LinkedHashMap后处理的回调
return null;
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();//初始化表或扩容
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//将单链表转化为双向链表
TreeNode<K,V> p = replacementTreeNode(e, null);//初始化当前节点,
if (tl == null)
hd = p;//设置头节点
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);//转红黑树
}
}
/**
*初始化或增加表大小。 如果为null,则根据字段阈值中保持的初始容量目标进行分配;否则,由于我们使用的是2的幂扩展,因此每个bin中的元素必须保持相同的索引,或者以2的幂偏移 新table。
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
//红黑树 节点类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
/**
* 当前链表头转红黑树
* @return root of tree
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
/**
* 当前红黑树转链表头
* @return root of tree
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
}
}
伸展树 Splay Tree
自我平衡的二叉查找树
堆 heep
用数组实现的二叉树(平衡二叉树),所以它没有使用父指针或者子指针。堆根据“堆属性”来排序,“堆属性”决定了树中节点的位置。 根索引为 i(i=0.为树根节点) ,则左子树索引 = 2*i+1,右子树索引=2*i+2;
容器类:PriorityQueue
//插入
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
//插入与父类比较大小换位
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
// child 的 index 为
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
public E peek() {
return (size == 0) ? null : (E) queue[0];
}
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
return result;
}
private E removeAt(int i) {
// assert i >= 0 && i < size;
modCount++;
int s = --size;
if (s == i) // removed last element
queue[i] = null;
else {
E moved = (E) queue[s];
queue[s] = null;
siftDown(i, moved);
if (queue[i] == moved) {
siftUp(i, moved);
if (queue[i] != moved)
return moved;
}
}
return null;
}
哈夫曼树
最优路径树,是通过叶子节点推算父节点,两个叶子节点(从小到大)之和为父节点,存在该值直接获取,不存在就构建父节点
-
路径与路径长度
: 从树中一个节点到另一个节点之间的分支构成了两个节点之间的路径,路径上的分支数目称作路径长度。若规定根节点位于第一层,则根节点到第H层的节点的路径长度为H-1。如到40 的路径长度为1;30的路径长度为2;20的路径长度为3。 -
节点的权
: 将树中的节点赋予一个某种含义的数值作为该节点的权值,该值称为节点的权; -
带权路径长度
: 从根节点到某个节点之间的路径长度与该节点的权的乘积。例如上图节点10的路径长度为3,它的带权路径长度为10 * 3 = 30; -
树的带权路径长度
: 树的带权路径长度为所有叶子节点的带权路径长度之和,称为WPL。上图的WPL = 1x40+2x30+3x10+3x20 = 190,而哈夫曼树就是树的带权路径最小的二叉树。
哈夫曼编码 : 从根节点到每一个叶子节点的路径上,左分支记为0,右分支记为1,将这些0与1连起来即为叶子节点的哈夫曼编码
节点 | 哈夫曼编码 |
---|---|
10 | 100 |
20 | 101 |
30 | 11 |
40 | 0 |
应用:文件压缩
Trie树 前缀数、字典数
根据字符前缀进行分支扩展管理,类似Hash表
应用:关键字查询
B树
B树(英语: B-tree)是一种自平衡的树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。B树,概括来说是一种自平衡的m阶树,与自平衡二叉查找树不同,B树适用于读写相对大的数据块的存储系统,例如磁盘。
- 根结点至少有两个子女 (度),根据度的最大值 进行扩展衍生。
- 每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m
- 每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
- 所有的叶子结点都位于同一层。
- 每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。
B+树
-
B+ 树是一种树数据结构,通常用于关系型数据库(如Mysql)和操作系统的文件系统中。B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。B+ 树元素自底向上插入,这与二叉树恰好相反。
-
在B树基础上,为叶子结点增加链表指针(B树+叶子有序链表),所有关键字都在叶子结点 中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中。
-
b+树的非叶子节点不保存数据,只保存子树的临界值(最大或者最小),所以同样大小的节点,b+树相对于b树能够有更多的分支,使得这棵树更加矮胖,查询时做的IO操作次数也更少。
将上一节中的B-Tree优化,由于B+Tree的非叶子节点只存储键值信息,假设每个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构如下图所示
5阶B+树
R树
R树是用来做空间数据存储的树状数据结构。例如给地理位置,矩形和多边形这类多维数据建立索引。
R树的核心思想是聚合距离相近的节点并在树结构的上一层将其表示为这些节点的最小外接矩形(MBR),这个最小外接矩形就成为上一层的一个节点。因为所有节点都在它们的最小外接矩形中,所以跟某个矩形不相交的查询就一定跟这个矩形中的所有节点都不相交。叶子节点上的每个矩形都代表一个对象,节点都是对象的聚合,并且越往上层聚合的对象就越多。也可以把每一层看做是对数据集的近似,叶子节点层是最细粒度的近似,与数据集相似度100%,越往上层越粗糙。
图
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为: G(V,E),其中,G(Graph)表示一个图,V(Vertices set)是图G中顶点的集合,E(Edges set)是图G中边的集合
名词
顶点
: 图中的各个点称之为顶点
边
:每个顶点与另一个顶点的关联关系称为边。
度
: degree,每个顶点所拥有边的数量。分类:根据边的指向关系可分为
有向图
(Directed graphs) : 任意两个顶点之间的边都是有向边;有向图中的边使用尖括号“<>”表示; 比如/<V1,V2>;有向图的度又分为入度(In-degree) :指向该顶点的边的数量
出度(Out-degree) :从改顶点出发的边的数量
无向图
(Undirected graphs) :任意两个顶点之间的边都是无向边;无向图中的边使用小括号“()”表示; 比如 (V1,V2)
权
Weight,每条边所具有的权重数。
自环
(Loop):若一条边的两个顶点相同,则此边称作自环。
路径
(Path):从顶点u到顶点v的一条路径
连通图
: 在无向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该无向图为连通图。
强连通图
: 在有向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该有向图为强连通图。
连通网
: 在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网。
生成树
: 一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。
最小生成树
: 在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。
存储结构
邻接矩阵表示法
图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边的信息。
优点:简单,结构清晰明了
缺点:由于存在n个顶点的图需要n*n个数组元素进行存储,当图为稀疏图时,使用邻接矩阵存储方法将会出现大量0元素,这会造成极大的空间浪费。
邻接表表示法
邻接表由表头节点和表节点两部分组成,图中每个顶点均对应一个存储在数组中的表头节点。如果这个表头节点所对应的顶点存在邻接节点,则把邻接节点依次存放于表头节点所指向的单向链表中。类似与hashMap 数组+链表的形式
遍历算法
广度优先算法 BFS Breadth-First Search
从顶点开始,分别访问邻接顶点,以层级顺利进行访问
深度优先算法 DFS Depth-First Search
从顶点开始,对单个邻接顶点访问一直至最低层,后返回访问其他未访问顶点依旧至最底层