赞
踩
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
}
return j;
}
#### 静态链表的优缺点 ![](https://github.com/Ewenwan/reading-notes/tree/master/da-hua-shu-ju-jie-gou/03-list/assets/static_link_list.png) 链表代码是最考验逻辑思维能力的。因为,链表代码到处都是指针的操作、边界条件的处理,稍有不慎就容易产生 Bug。链表代码写得好坏,可以看出一个人写代码是否够细心,考虑问题是否全面,思维是否缜密。所以,这也是很多面试官喜欢让人手写链表代码的原因。所以,这一节讲到的东西,你一定要自己写代码实现一下,才有效果。 ### 栈 stack ![](https://camo.githubusercontent.com/60336f237299d395ccd09a1e0a5dd3130d388789/68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f33652f30622f33653230636361303332633235313638643363633630356661376135336130622e6a7067) 栈–是限定仅在表尾进行插入和删除操作的线性表 人生,就像一个很大的栈演变。出生时赤条条地来到这个世界,慢慢地长大,渐渐地变老,最终还得赤条条地离开世间。 后进先出 LIFO结构(Last In First Out) 弹夹 网页后退功能 栈顶–允许插入和删除的那一端 重要–进栈出栈变化形式–不是等所有元素进栈后再出栈—可以先进栈的元素直接出栈–后面的元素再进栈出栈 抽象数据类型
ADT 栈(stack)
Data
同线性表,元素具有相同的类型,相邻元素具有前驱和后继关系
Operation
InitStack(*S):初始化操作,建立一个空栈S
DestroyStack(*S):若栈存在,则销毁它
ClearStack(*S):将栈清空
StackEmpty(S):若栈为空,返回true,否则返回false
GetTop(S,*e):若栈存在且非空,用e返回S的栈顶元素
Push(*S,e):若栈存在,插入新元素e到栈S中并成为栈顶元素
Pop(*S,*e):删除栈S中栈顶元素,并用e返回其值
StackLength(S):返回栈S的元素个数
endADT
顺序栈–栈的顺序存储结构–栈顶不变–栈顶位置变化—游标卡尺
有一种有趣的两栈共享空间,两端为栈底,两个茶杯口对口
链栈–栈的链式存储结构 不需要头结点
typedef struct StackNode{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStackPtr;
typedef struct LickStack{
LinkStackPtr top;
int count;
}LinkStack;
//说明在声明变量的时候可以直接StackNode stack1,而不是struct StackNode stack1,不写第一个StackNode也可以
//说明此结构体类的结构体指针为LinkStackPtr
如果栈中的元素变化不可预料,时大时小,用链栈 如果在可控范围内的变化,用顺序栈 迭代–常规for循环 递归–调用自己的函数称为递归函数 菲波那切数列兔子问题–前面相邻两项之和,一年后144只兔子 栈的应用:
逆波兰表示法: 定义后缀表达式,计算四则运算 9+(3-1)3+10/2 --> 9 3 1-3+10 2/+
后缀表达式运算规则:
从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,
就将处于栈顶两个数字出栈,进行运算,运算后的结果再进栈,一直到获得结果
中缀表达式转后缀表达式规则:
从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;
若是符号,则判断其与栈顶符号的优先级,是右符号或优先级低于栈顶符号(*/优先±),
则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止
### 队列 queue [参考2](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb) 队列–是只允许在一端进行插入操作,而在另一端进行删除操作的线性表 是一种先进先出的线性表(First In First Out)简称FIFO 线程池等有限资源池 阻塞队列和并发队列 允许插入的一端称为队尾,允许删除的一端称为队头
ADT 队列(Queue)
Data
同线性表.元素具有相同的类型,相邻元素具有前驱和后继关系
Operation
InitQueue(*Q):初始化操作,建立一个空队列Q
DestroyQueue(*Q):若队列Q存在,则销毁它
ClearQueue(*Q):将队列Q清空
QueueEmpty(Q):若队列Q为空,返回true,否则返回false
GetHead(Q,*e):若队列Q存在且非空,用e返回队列Q的队头元素
EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素
DeQueue(*Q,*e):删除队列Q中队头元素,并用e返回其值
QueueLength(Q)返回队列Q的元素个数
endADT
循环队列的顺序存储结构
typedef int QElemType;
typedef struct{
QElemType data[MAXSIZE];
int front; //头指针
int rear; //尾指针,若队列不空,指向队列尾元素的下一个位置
}SqQueue;
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出,简称为链队列 ### 串–专门存储字符的字符数组字符序列? 串–是由零个或多个字符组成的有限序列,又名字符串 空格串: 只包含空格的串 子串: 串中任意个数的连续字符组成的子序列 主串: 包含子串的串
ADT 串(string)
Data
串中元素仅由一个字符组成,相邻元素具有前驱和后继关系
Operation
StrAssign(T,*chars) 生成一个其值等于字符串常量chars的串T
StrCopy(T,S) 串S存在,由串S复制得到串T
ClearString(S) 串S存在,将串清空
StringEmpty(S) 若串S为空,返回true,否则返回false
StrLength(S) 返回串S的元素个数,即串的长度
StrCompare(S,T) 若S>T,返回值大于0,若S=T,返回0,若S<T,返回值<0
Concat(T,S1,S2) 用T返回由S1和S2联接而成的新串
SubString(Sub,S,index,len) 串S存在,1<=index<=StrLength(S),且0<=index<=StrLength(S)-index+1,用Sub返回串S的第index个字符起长度为len的子串
Index(S,T,index) 串S和T存在,T是非空串,1<=index<=StrLength(S),若主串S中存在串T值相同的子串,则返回它在主串S中第index个字符之后第一次出现的位置,否则返回0
Replace(S,T,V) 串S,T,V存在,T是非空串.用V替换主串S中出现的所有与T相等的不重叠的子串
StrInsert(S,index,T) 串S和T存在,1<=index<=StrLength(S)+1,在串S的第index个字符之前插入串T
StrDelete(S,index,len) 串S存在,1<=index<=StrLength(S)-len+1,从串S中删除第index个字符起长度为len的子串
endADT
朴素模式匹配算法: 子串的定位操作 挨个遍历
KMP模式匹配算法:大大避免重复遍历的情况 克努特-莫里斯-普拉特算法 KMP算法
把T串各个位置的j值得变化定义为一个数组next,next的长度就是T串的长度
next[j] = 0 (当j=1时)
= k (“P1…P(k-1)” = “P(j-k+1)…P(j-1)”)
= 1 (其他情况)
j 123456
T abcdex
next[j] 011111
j 123456
T abcabx
next[j] 011123
j 123456789
T ababaaaba
next[j] 011234223
优化KMP模式匹配算法:
在计算next值时,判断T[next]的值与当前位置T[j]的值是否相等,如果相等,
nextval[j]的值就等于nextval[next]的值
j 123456789
T ababaaaba
next[j] 011234223
nextval[j] 010104210
## 4. 非线性表 线性表 ![](https://camo.githubusercontent.com/cdb8cc4615574d83d184a34f6392367e3b39f994/68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f62362f37372f62366237316563343639333531333064666635633462363263663237333437372e6a7067) 非线性表,比如二叉树、堆、图等。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。 ![](https://camo.githubusercontent.com/a839b06930515eee042f0cbbbc63dad8e6345146/68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f36652f36392f36656266343236343162356639386639313264333666366266383666363536392e6a7067) ### 树 tree [参考](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb) ![](https://camo.githubusercontent.com/51a8dc13d22c5e1c273d7bb76c66e66c7c833fa1/68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f62372f32392f62373034336266323961323533626233363232316561656336326232653132392e6a7067) 树–是n个节点的有限集.n=0时为空树 在任意一棵非空树中:
1>有且仅有一个特定的称为根(root)的结点
2>当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2…Tm,
其中每一个集合本身又是一棵树,并且称为根的子树
结点拥有的子树数称为结点的度,
度为0的结点称为 叶结点 或 终端结点,
度不为0的称为非终端结点或分支结点,
除根结点外,也称为内部结点
结点的层次从根开始定义起,
根为第一层,
树中结点的最大层次称为树的深度或高度
如果将树中结点的各子树看成从左至右有次序的,
不能互换的,
则称该树为有序树
![](https://camo.githubusercontent.com/e0f9e46fd06bbce8ef86192d909e2e6166ab1416/68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f35302f62342f35306638393531306164316637353730373931646431326634653961646562342e6a7067) “高度”这个概念,其实就是从下往上度量,比如我们要度量第 10 层楼的高度、第 13 层楼的高度,起点都是地面。所以,树这种数据结构的高度也是一样,从最底层开始计数,并且计数的起点是 0。 “深度”这个概念在生活中是从上往下度量的,比如水中鱼的深度,是从水平面开始度量的。所以,树这种数据结构的深度也是类似的,从根结点开始度量,并且计数起点也是 0。 “层数”跟深度的计算类似,不过,计数起点是 1,也就是说根节点的位于第 1 层。 抽象数据类型
ADT 树(tree)
Data
树由一个根结点和若干棵子树构成,树中结点具有相同数据类型及层次关系
Operation
InitTree(*T) 构造空树T
DestroyTree(*T) 销毁树T
CreateTree(*T,definition) 按definition中给出的树的定义来构造树
ClearTree(*T) 若树T存在,则将树T清为空树
TreeDepth(T) 返回T的深度
Root(T) 返回T的根结点
Value(T,cur_e) cur_e是树T中一个结点,返回此结点的值
Assign(T,cur_e,value) 给树T中结点cur_e赋值为value
Parent(T,cur_e) 若cur_e是树T的非根结点,则返回它的双亲,否则返回空
LeftChild(T,cur_e) 若cur_e是树T的非根结点,则返回它的最左孩子,否则返回空
RightSibling(T,cur_e) 若cur_e有右兄弟,则返回它的右兄弟,否则返回空
InsertChild(*T,*p,i,c) 其中p指向树T的某个结点,i为所指结点p的度加上1,
非空树c与T不相交,操作结果为插入c为树T中p指结点的第i棵子树
DeleteChild(*T,*p,i) 其中p指向树T的某个结点,i为所指结点p的度,
操作结果为删除T中所指节点的第i棵子树
endADT
树的存储结构:
1 双亲表示法:
#define MAX_TREE_SIZE 100
typedef int TElemType; //树结点的数据类型
typedef struct PTNode{ //结点结构
TElemType data; //结点数据
int parent; //双亲位置
}PTNode;
typedef struct{ //树结构
PTNode nodes[MAX_TREE_SIZE]; //结点数组
int r,n; //根的位置和结点数
}PTree;
可以扩展域,增加双亲域,长子域,兄弟域
2 孩子表示法:
多重链表,每个结点有多个指针域,每个指针指向一棵子树的根节点
2.1 每个结点指针域的个数等于树的度 牺牲空间
2.2 每个结点指针域的个数等于该结点的度 牺牲时间
2.3 每个结点的孩子结点排列起来,以单链表作存储结构,
则n个结点有n个孩子链表,然后n个头指针又组成一个线性表,
顺序存储结构,存放进一个一维数组中
#define MAX_TREE_SIZE 100
typedef struct CTNode{ //孩子结点
int child;
struct CTNode *next;
}*ChildPtr;
typedef struct{ //表头结构
TElemType data;
ChildPtr firstchild;
}CTBox;
typedef struct{ //树结构
CTBox nodes[MAX_TREE_SIZE]; //结点数组
int r,n; //根的位置和结点数
}CTree;
3.孩子兄弟表示法
typedef struct CSNode{
TElemType data;
struct CSNode *firstChild,*rightSib;
}CSNode,*CSTree;
![](https://camo.githubusercontent.com/cf5bc6b15320af67a3b80c13654e8d2f56cb1a38/68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f31322f38652f31326364313162323433326564376334646663396132303533636237306238652e6a7067)
#### 二叉树–
[参考](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)
是n(n>=0)个结点的有限集合,
该集合或者为空集(称为空二叉树),
或者由一个根结点和两棵互不相交的,
分别称为根结点的左子树和右子树的二叉树组成
每个结点最多有两棵子树,左子树和右子树是有顺序的,
即使只有一棵子树,也要区分左还是右,且次序不能颠倒
线性表结构可以理解为是树的一种极其特殊的表现形式
左斜树,右斜树,满二叉树,完全二叉树(按层序顺序编号)
二叉树性质:
1.在二叉树的第i层上至多有2^(i-1)个结点
2.深度为k的二叉树至多有2^k -1个结点
3.对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2 + 1
4.具有n个结点的完全二叉树的深度为[log2^n]+1
5.寻找双亲为i/2等
二叉树的顺序存储结构
适用于完全二叉树,按顺序存入数组中 ABCDEFGHI
二叉链表
二叉树每个结点最多有两个孩子,即一个数据域和两个指针域
typedef struct BiTNode{ //结点结构
TElemType data; //结点数据
struct BiTNode *lChild,*rChild; //左右孩子指针
}BiTNode,*BiTree;
二叉树的遍历,是指从根结点出发,
按照某种次序依次访问二叉树中所有结点,
使得每个结点被访问一次且仅被访问一次
![](https://camo.githubusercontent.com/f5bff6fd9208fe6fda2ce71f820ceedad7f2527f/68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f61622f31362f61623130333832326537356235623135633631356236383536306362323431362e6a7067)
1>前序遍历
先访问根节点,然后前序遍历左子树,再前序遍历右子树 ABDGHCEIF
void PreOrderTraverse(BiTree T){
if(T==NULL){
return;
}
printf(“%c”,T->data);
PreOrderTraverse(T->lCHild);//先遍历左子树
PreOrderTraverse(T->rChild);//然后遍历右子树
}
2>中序遍历
从根结点开始,中序遍历根结点的左子树,然后访问根结点,最后中序遍历右子树 GDHBAEICF
void PreOrderTraverse(BiTree T){
if(T==NULL){
return;
}
PreOrderTraverse(T->lCHild);//先遍历左子树
printf(“%c”,T->data);
PreOrderTraverse(T->rChild);//然后遍历右子树
}
3>后序遍历
从左到右先叶子后结点遍历左右子树,最后访问根结点 GHDBIEFCA
void PreOrderTraverse(BiTree T){
if(T==NULL){
return;
}
PreOrderTraverse(T->lCHild);//先遍历左子树
PreOrderTraverse(T->rChild);//然后遍历右子树
printf(“%c”,T->data);
}
4>层序遍历 从根结点开始访问,从上而下逐层遍历,在同一层中,从左到右对结点逐个访问 ABCDEFGHI 建立二叉树: 将二叉树中每个结点的空指针引出一个虚结点,其值为"#",称为扩展二叉树 AB#D##C## 线索二叉树: 把空余的指针域指向前驱后继元素,指向前驱和后继的指针称为线索, 加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树 并且除了两个左右指针域,再增加两个标识BOOL值,lTag与rTag,lTag为0时指向左孩子, 为1时指向前驱,rTag为0时指向右孩子,为1时指向后继
typedef enum(Link,Thread) PointerTag; //Link0表示指向左右孩子指针 Thread1表示指向前驱或后继的线索
typedef struct BiTNode{ //结点结构
TElemType data; //结点数据
struct BiTNode *lChild,*rChild; //左右孩子指针
PointerTag lTag; //左标识
PointerTag rTag; //右标识
}BiTNode,*BiTree;
树转换为二叉树:
1.加线.在所有兄弟结点之间加一条连线
2.去线.每个结点只保留与第一个孩子结点的连线,删除它与其他孩子结点之间的连线
3.第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子
森林转换为二叉树:
1.把每个树转换为二叉树
2.把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子
二叉树转换为树:
1.加线.将子级与孙子级的右孩子结点都作为此结点的孩子连接
2.去线.删除所有结点与其右孩子结点的连线
二叉树转换为森林:
1.把所有右孩子分离出来
2.把分离出来的二叉树转换为树
森林的前序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同
赫夫曼树
是压缩文件时用的最基本的压缩编码方法
树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称为路径长度
结点的带权路径长度为从该结点到树根之间的路径长度与结点上权(值比例)的乘积
带权路径长度WPL最小的二叉树称作赫夫曼树.最优二叉树.
构造方法:
1.把各结点按照权值从小到大排列
2.取最小权值的两个结点作为新结点N1的两个子结点,新结点N1的权值为两个叶子权值得和
3.将N1替换两个结点重新排列
4.重复以上三步
传递时,构造好赫夫曼树,双方约定好赫夫曼编码规则,按照路径寻找数据 一般规定赫夫曼树的左分支代表0,右分支代表1,从根结点到叶子结点经过的路径分支组成的0和1的序列便为该结点对应字符的编码,这就是赫夫曼编码 ### 图 社交网络 如何存储微博、微信等这些社交网络的好友关系? [参考](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb) ![](https://camo.githubusercontent.com/4bebe8ccec1c5c2d0772578cdd99d59558d2e3c6/68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f64662f61662f64663835646333343561393732366361623033333865363839383266643161662e6a7067) 是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E), 其中,G表示一个图,V是图G中顶点的集合,E是图G中边集合 顶点(元素)之间的边没有方向,则称这条边为无向边(),全是无向边的图称为无向图 G(V,{E}) V={A,B,C,D} E={(A,D),(B,A),(C,A),(B,C)} 有方向则为有向边<>,也称为弧,弧头,弧尾,有向图 G(V,{E}) V={A,B,C,D} E={<A,D>,<B,A>,<C,A>,<B,C>}
简单图:无重复边与指向自身的边
稀疏图,稠密图,连通图,子图,
权:与图的边或弧相关的数
网:带权的图
无向完全图:任意两个顶点之间都存在边 n(n-1)/2条边
有向完全图:任意两个顶点之间都存在方向互为相反的两条弧 n(n-1)条边
无向图的度,有向图的入度,出度
ADT 图
Data
顶点的有穷非空集合和边的集合
Operation
CreateGraph(*G,V,VR) 按照顶点集V和边弧VR的定义构造图G
DestroyGraph(*G) 图G存在则销毁
LocateVex(G,u) 若图G中存在顶点u,则返回图中的位置
GetVex(G,v) 返回图G中顶点v的值
PutVex(G,v,value) 将图G中顶点v赋值value
FirstAdjVex(G,*v) 返回顶点v的一个邻接顶点,若顶点在G中无邻接顶点返回空
NextAdjVex(G,v,*w) 返回顶点v相对于顶点w的下一个邻接顶点,若w是v的最后一个邻接点则返回空
InsertVex(*G,v) 在图G中增添新顶点v
DeleteVex(*G,v) 删除图G中顶点v及其相关的弧
InsertArc(*G,v,w) 在图G中增添弧<v,w>,若G是无向图,还需添加对称弧<w,v>
DeleteArc(*G,v,w) 在图G中删除弧<v,w>,若G是无向图,还需删除对称弧<w,v>
DFSTraverse(G) 对图G中进行深度优先遍历,在遍历过程对每个顶点调用
HFSTraverse(G) 对图G中进行广度优先遍历,在遍历过程对每个顶点调用
endADT
存储方式 > > 1.邻接矩阵 > > > 用两个数组,一个一维数组存储图中顶点信息,一个二维数组(邻接矩阵)存储图中的边或弧的信息 无向图的边数组是一个对称矩阵 ![](https://camo.githubusercontent.com/4e6d20c7ee0ddd27fac250b9609973e7356d80e0/68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f36322f64322f36323565373439336235343730653737346235616139316662346664623964322e6a7067) > > 2.邻接表—链表数组– > > > 针对有向图,用数组与链表结合,一个一维数组存储图中顶点信息,每个顶点的所有邻接点构成一个线性表(入度) 逆邻接表(出度) ![](https://camo.githubusercontent.com/20bdf1e1e2ae9756f8643a38652d3306f2939212/68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f35302f61312f35303134343062636666646366346536663961356361313131376539393061312e6a7067) > > 3.十字链表 > > > 把邻接表和逆邻接表整合在了一起 > > 4.邻接多重表 > > > 针对无向图,根据十字链表设计 > > 5.边集数组 > > > 由两个一维数组构成,一个存储顶点的信息,另一个存储边的信息,这个边数组每个数据元素包括起点下标,终点下标,权 > > 最短路径 > > > 深度和广度优先搜索:如何找出社交网络中的三度好友关系? 还有 A\*、D\*、IDA\* 等启发式图上搜索算法。
// java
public class Graph { // 无向图
private int v; // 顶点的个数
private LinkedList adj[]; // 邻接表 链表数组
public Graph(int v) {
this.v = v;
adj = new LinkedList[v]; // 链表数组
for (int i=0; i<v; ++i) {
adj[i] = new LinkedList<>();//单个链表
}
}
public void addEdge(int s, int t) { // 无向图一条边存两次
adj[s].add(t);
adj[t].add(s);
}
}
> > 1.深度优先遍历 DFS > > > 广度优先搜索(Breadth-First-Search),我们平常都把简称为 BFS。直观地讲,它其实就是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。 ![](https://camo.githubusercontent.com/c9f5b8952b4d84a86760af75b07cc90e3ad28e68/68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f30302f65612f30303265396535346662306434646266353436323232366439343666613165612e6a7067)
// 其中 s 表示起始顶点,t 表示终止顶点。我们搜索一条从 s 到 t 的路径。
// 实际上,这样求得的路径就是从 s 到 t 的最短路径。
public void bfs(int s, int t) {
if (s == t) return;
boolean[] visited = new boolean[v];
visited[s]=true;
Queue queue = new LinkedList<>();
queue.add(s);
int[] prev = new int[v];
for (int i = 0; i < v; ++i) {
prev[i] = -1;
}
while (queue.size() != 0) {
int w = queue.poll();
for (int i = 0; i < adj[w].size(); ++i) {
int q = adj[w].get(i);
if (!visited[q]) {
prev[q] = w;
if (q == t) {
print(prev, s, t);
return;
}
visited[q] = true;
queue.add(q);
}
}
}
}
private void print(int[] prev, int s, int t) { // 递归打印 s->t 的路径
if (prev[t] != -1 && t != s) {
print(prev, s, prev[t]);
}
System.out.print(t + " ");
}
visited是用来记录已经被访问的顶点,用来避免顶点被重复访问。如果顶点 q 被访问,那相应的 visited[q] 会被设置为 true。 queue是一个队列,用来存储已经被访问、但相连的顶点还没有被访问的顶点。因为广度优先搜索是逐层访问的,也就是说,我们只有把第 k 层的顶点都访问完成之后,才能访问第 k+1 层的顶点。当我们访问到第 k 层的顶点的时候,我们需要把第 k 层的顶点记录下来,稍后才能通过第 k 层的顶点来找第 k+1 层的顶点。所以,我们用这个队列来实现记录的功能。 prev用来记录搜索路径。当我们从顶点 s 开始,广度优先搜索到顶点 t 后,prev 数组中存储的就是搜索的路径。不过,这个路径是反向存储的。prev[w] 存储的是,顶点 w 是从哪个前驱顶点遍历过来的。比如,我们通过顶点 2 的邻接表访问到顶点 3,那 prev[3] 就等于 2。为了正向打印出路径,我们需要递归地来打印,你可以看下 print() 函数的实现方式。 ![](https://camo.githubusercontent.com/4a9d706a4994085ee27434148affbba6db72aaac/68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f34662f33612f34666561386334353035623334326366616638636230613933613635353033612e6a7067) > > 2.广度优先遍历 BFS > > > 深度优先搜索(Depth-First-Search),简称 DFS。最直观的例子就是“走迷宫”。 假设你站在迷宫的某个岔路口,然后想找到出口。你随意选择一个岔路口来走,走着走着发现走不通的时候,你就回退到上一个岔路口,重新选择一条路继续走,直到最终找到出口。这种走法就是一种深度优先搜索策略。 走迷宫的例子很容易能看懂,我们现在再来看下,如何在图中应用深度优先搜索,来找某个顶点到另一个顶点的路径。 你可以看我画的这幅图。搜索的起始顶点是 s,终止顶点是 t,我们希望在图中寻找一条从顶点 s 到顶点 t 的路径。如果映射到迷宫那个例子,s 就是你起始所在的位置,t 就是出口。 我用深度递归算法,把整个搜索的路径标记出来了。这里面实线箭头表示遍历,虚线箭头表示回退。从图中我们可以看出,深度优先搜索找出来的路径,并不是顶点 s 到顶点 t 的最短路径。 ![](https://camo.githubusercontent.com/4387149f77cdb67c826d6fe79edfe09de9e96513/68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f38372f38352f38373738323031636536666637303337633062336632366238336566626138352e6a7067) 实际上,深度优先搜索用的是一种比较著名的算法思想,回溯思想。这种思想解决问题的过程,非常适合用递归来实现。 我们发现,深度优先搜索代码实现也用到了 prev、visited 变量以及 print() 函数,它们跟广度优先搜索代码实现里的作用是一样的。不过,深度优先搜索代码实现里,有个比较特殊的变量 found,它的作用是,当我们已经找到终止顶点 t 之后,我们就不再递归地继续查找了。
boolean found = false; // 全局变量或者类成员变量
public void dfs(int s, int t) {
found = false;
boolean[] visited = new boolean[v];
int[] prev = new int[v];
for (int i = 0; i < v; ++i) {
prev[i] = -1;
}
recurDfs(s, t, visited, prev);
print(prev, s, t);
}
private void recurDfs(int w, int t, boolean[] visited, int[] prev) {
if (found == true) return;
visited[w] = true;
if (w == t) {
found = true;
return;
}
for (int i = 0; i < adj[w].size(); ++i) {
int q = adj[w].get(i);
if (!visited[q]) {
prev[q] = w;
recurDfs(q, t, visited, prev);
}
}
}
广度优先搜索,通俗的理解就是,地毯式层层推进,从起始顶点开始,依次往外遍历。 广度优先搜索需要借助队列来实现,遍历得到的路径就是,起始顶点到终止顶点的最短路径。 深度优先搜索用的是回溯思想,非常适合用递归实现。换种说法,深度优先搜索是借助栈来实现的。 在执行效率方面,深度优先和广度优先搜索的时间复杂度都是 O(E),空间复杂度是 O(V)。 深度和广度搜索各有优缺点,就像人生在追求卓越时,着重深度还是广度是很难说得清楚的。 一个连通图的生成树是一个极小的连通子图,它含有图中全部的顶点,但只有足以构成一棵树的n-1条边. 最小生成树–构造连通网的最小代价生成树 > > 普里姆算法 > > > 假设N=(P,{E})是连通网,TE是N上最小生成树中边的集合.算法从U={u0}(u0属于V),TE={}开始. 在所有u属于U,v属于V-U的边(u,v)属于E中,找出一条代价最小的边(u0,v0)并入集合TE,同时v0并入U,直至U=V为止.此时TE中必有n-1条边,则T=(V,{TE})为N的最小生成树. 时间复杂度为O(n²) 针对顶点展开,边数多的情况下更好一些 > > 克鲁斯卡尔算法 > > > 假设N=(V,{E})是连通网,则令最小生成树的初始状态为只有n个顶点而无边的非连通图T={V,{}},图中每个顶点自成一个连通分量.在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入到T中,否则舍去此边选择下一条代价最小的边.直至T中所有的顶点都在同一连通分量上为止 时间复杂度为O(eloge) 针对边来展开,边数少时效率会非常高 > > 最短路径 > > > 非网图指的是两顶点之间经过的边数最少的路径, 网图指的是两顶点之间经过的边上权值之和最少的路径 > > 迪杰斯特拉算法 > > > 按路径长度递增的次序产生最短路径的算法 时间复杂度为O(n²) > > 弗洛伊德算法 > > > 列出两个矩阵,一个是顶点到顶点的最短路径权值和的矩阵,一个是对应顶点的最小路径的前驱矩阵(初始化为P[i][j]=j存储路径) 通过修改初始矩阵的值,最终得到可以找出最短路径的矩阵 时间复杂度为O(n³) > > 拓扑排序 > > > 针对表示工程的有向图,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,称为AOV网 设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列v1,v2…vn,满足若从顶点vi到vj有一条路径,顶点vi必在vj之前,该顶点序列为一个拓扑序列 拓扑排序,对有向图构造拓扑序列的过程,解决一个工程能否顺序进行的问题 从AOV网中选择入度为0的顶点入栈,选择栈顶的顶点输出,然后删去此顶点,并删除此顶点为尾的弧,出现了入度为0的顶点就继续入栈,直到输出全部顶点或者AOV网中不存在入度为0的顶点,这个顺序就是拓扑排序方案 > > 关键路径 > > > 在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,称为AOE网 其实就是从起点到终点找最长路径 ## 查找 静态查找表:只做查找操作的查找表 动态查找表:在查找过程中有增删操作的 顺序查找:从第一个查找到最后一个,直到查找到为止 > > 有序表查找 > > > 1. 折半查找: [参考](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb) 二分查找,线性表中的数据必须是有序的,采用顺序存储.取中间数据作为比较对象,若小于中间数据,再左半区继续查找,若大于中间数据,在右半区继续查找. 查找思想有点类似分治思想。 即每次都通过跟区间中的中间元素对比,将待查找的区间缩小为一半,直到找到要查找的元素,或者区间被缩小为 0。但是二分查找的代码实现比较容易写错。你需要着重掌握它的三个容易出错的地方:循环退出条件、mid 的取值,low 和 high 的更新。 下标mid = low + 1/2 \* (high-low) 凡是用二分查找能解决的,绝大部分我们更倾向于用散列表或者二叉查找树。 即便是二分查找在内存使用上更节省,但是毕竟内存如此紧缺的情况并不多。 2. 插值查找: 根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法 mid = (key-a[low]) / (a[high]-a[low]) \* (high-low) 表长较大,关键字分布较均匀的表,平均性能比折半查找好得多 3. 斐波那契查找: 根据要查找表的长度n处于斐波那契数列F={0,1,1,2,3,5,8,13,21…}中位置, 计算出k值(n=10,F[6]<n<F[7],得出k=7) key<a[mid]时,新范围是low到mid-1,个数为F[k-1]-1个 key>a[mid]时,新范围是mid+1到high,个数为F[k-2]-1个 mid = low + F[k-1] - 1 索引:
加快查找速度设计的一种数据结构,把一个关键字与它对应的数据相关联的过程
分为线性索引,树形索引,多级索引
线性索引:
索引表,将索引项集合组织为线性结构
分为稠密索引,分块索引,倒排索引
稠密索引:
在线性索引中将数据集中的每个记录对应一个索引项
索引表索引项是按照关键码有序的排列
索引项有序,在查找关键字时,可以使用有序查找算法
分块索引:
对数据进行分段,每个分块有序,每一个块建立一个索引项,减少索引项的个数,块内无序,块间有序
一个分块索引表分为三部分,最大key值,块长,块首指针
倒排索引:
记录号表存储具有相同次关键字的所有记录的记录号
二叉排序树:
又称二叉查找树,左子树的值<根结点的值<右子树的值
中序查找时,就如同顺序查找了
平衡二叉树 AVL树:
一种二叉排序树,每一个节点的左子树和右子树的 高度差 至多为1
看起来是对称的,可以提供比较高效的查找效率
多路查找树 B树:
每个结点的孩子数可以多于两个,每个结点处可以存储多个元素
对数据的处理不断从硬盘等存储设备中调入或调出内存页面
2-3树 3阶B树:
每个结点都有两个孩子或三个孩子
一个2结点包含一个元素和两个孩子
一个3结点包含一大一小两个元素和三个孩子
2-3-4树 4阶B树:
一个4结点包含小中大三个元素和四个孩子
B树:
平衡的多路查找树,2-3树和2-3-4树都是B树的特例,结点最大的孩子数目称为B树的阶
为内外存的数据交互准备的
调整B树结点的元素与硬盘存储页面大小相匹配,把根结点持久地保留在内存中,
在这棵树上,寻找某一个关键字至多需要两次硬盘的读取
由于B树每个结点的元素比二叉树多得多,减少了必须访问结点和数据块的数量,从而提高了性能
B+树:
出现在分支结点中的元素,会在叶子结点中再次列出,每一个结点都会保存一个指向后一叶子结点的指针
应文件系统所需而出
散列表(哈希表)查找:
[参考](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)
f称为散列函数,哈希函数
关键字对应的记录存储位置称为散列地址
采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表
是面向查找的存储结构,散列技术的数据之间不存在什么逻辑关系,只与关键字有关
解决查找与给定值相等的记录
设计哈希表:
直接定址法,数字分析法,平方取中法,折叠法,除留余数法,随机数法
关键在于解决key冲突
开放地址法,
## 排序----购物网站商品排序— 排序算法太多了,有很多可能你连名字都没听说过,比如猴子排序、睡眠排序、面条排序等。 最经典的、最常用的:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。 插入排序和冒泡排序的时间复杂度相同,都是 O(n2),在实际的软件开发里,为什么我们更倾向于使用插入排序算法而不是冒泡排序算法呢? > > 冒泡排序: > ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fcamo.githubusercontent.com%2F968e735c629173a7122be263f926ce4cf189dfec%2F68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f34302f65392f34303338663634663437393735616239663531396534663733396534363465392e6a7067&pos_id=img-bRJvaZRU-1715464946230) > > > > > 插入排序 > ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fcamo.githubusercontent.com%2F05cba6defd5f15b37965cd20e7b052933ad67dbf%2F68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f37622f61362f37623235376531373937383763363333643262643137316137363431373161362e6a7067&pos_id=img-rQWmLDsA-1715464946230) > > > > > 选择排序 > ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fcamo.githubusercontent.com%2F5729b3eab2b2d2396cc1872adfbf948807d441fa%2F68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f33322f31642f33323337313437356130623038663064623938363164313032343734313831642e6a7067&pos_id=img-aAAfWU5q-1715464946231) > > > > > 归并排序 > ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fcamo.githubusercontent.com%2Fba4d37d583d5c43ba571063c8c8c5145e97de645%2F68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f64622f32622f64623766383932643333353565663734646139636436346161393236646332622e6a7067&pos_id=img-IFidMmLg-1715464946231) > > > > > 快速排序 > ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fcamo.githubusercontent.com%2F004f3a475b6086b29c3f7b84d8d4cce3f543ae1a%2F68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f34642f38312f34643839326333613265303861313766313630393764303765613038386138312e6a7067&pos_id=img-sBWyXVCs-1715464946231) > > > > > 归并排序 & 快速排序 > ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fcamo.githubusercontent.com%2F2c1a539bac5c6c6c28107b2889aaffb5a0fe8a49%2F68747470733a2f2f7374617469633030312e6765656b62616e672e6f72672f7265736f757263652f696d6167652f61612f30352f61613033616535373064616365343136313237633963636639646238616330352e6a7067&pos_id=img-gwEpnaT0-1715464946232) > > > ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190523233604147.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpYW94aWFvd2VucWlhbmc=,size_16,color_FFFFFF,t_70) ![img](https://img-blog.csdnimg.cn/img_convert/08a0e90bc369382d2d9ddba3d8856929.png) ![img](https://img-blog.csdnimg.cn/img_convert/81859d9a46698290a1f1b292a3e0bda0.png) **网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。** **[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)** **一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!** 数据之间不存在什么逻辑关系,只与关键字有关 解决查找与给定值相等的记录
设计哈希表:
直接定址法,数字分析法,平方取中法,折叠法,除留余数法,随机数法
关键在于解决key冲突
开放地址法,
排序算法太多了,有很多可能你连名字都没听说过,比如猴子排序、睡眠排序、面条排序等。
最经典的、最常用的:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。
插入排序和冒泡排序的时间复杂度相同,都是 O(n2),在实际的软件开发里,为什么我们更倾向于使用插入排序算法而不是冒泡排序算法呢?
冒泡排序:
[外链图片转存中…(img-bRJvaZRU-1715464946230)]
插入排序
[外链图片转存中…(img-rQWmLDsA-1715464946230)]
选择排序
[外链图片转存中…(img-aAAfWU5q-1715464946231)]
归并排序
[外链图片转存中…(img-IFidMmLg-1715464946231)]
快速排序
[外链图片转存中…(img-sBWyXVCs-1715464946231)]
归并排序 & 快速排序
[外链图片转存中…(img-gwEpnaT0-1715464946232)]
[外链图片转存中…(img-EuBCk3c8-1715464946232)]
[外链图片转存中…(img-q0tT5e1Z-1715464946232)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。