1.链表
(1)循环链表的判断问题
判断整个链表是否有环,如何找到这个环
提问:给定一个单链表,只给出头指针root:
1.如果判断是否存在环?
2.如何知道环的长度?
3.如何找出环的连接点在哪里?
4.带环链表的长度是多少
整体思路:快慢指针法,如果只让你判断是否存在环,还可以用破解链表法!!!
1.如果判断是否存在环?
public static Node judgeCircle(Node root){
Node fast=root;
Node low=root;
if(root==null){
return null;
}
while(fast.next!=null&&low!=null){
fast=fast.next.next;
low=low.next;
if(fast==low){
return fast;
}
}
return null;
}
2.如何知道环的长度?
public static int circleLength(Node meetNode){
int length=1;
Node fast=meetNode;
Node low=meetNode;
fast=fast.next.next;
low=low.next;
while(fast!=low){
fast=fast.next.next;
low=low.next;
length++;
}
return length;
}
3.如何找出环的连接点在哪里?
public static Node connectNode(Node root,Node meetNode){
Node lowStart=root;
Node lowMeet=meetNode;
while(lowStart!=lowMeet){
lowMeet= lowMeet.next;
lowStart=lowStart.next;
}
return lowMeet;
}
4.带环链表的长度是多少
public static int sumLength(Node root,Node connectNode){
int length=0;
while(root!=connectNode){
length++;
root=root.next;
}
root=root.next;
length++;
while(root!=connectNode){
root=root.next;
length++;
}
return length;
}
Main程序:
public static void main(String[] args) {
Node root=new Node(-1);
Node p=root;
Node record=null;
for(int i=0;i<10;i++){
Node q=new Node(i);
p.next=q;
p=q;
if(i==4){
record=p;
}
}
p.next=record;
//(1)判断是否含有环?
Node meetNode=judgeCircle(root);
if(meetNode!=null){
System.out.println(true);
}else{
System.out.println(false);
}
//(2)如何知道环的长度?
System.out.println("环的长度为:"+circleLength(meetNode));
//(3)如何找出环的连接点?
//通过画图,得出a=(n-1)(b+c)+c,所以可以让两个慢指针分别从出发点和快慢指针相聚点出发,
//两个指针遇见的地方就是环的连接点;
Node ConnectNode=connectNode(root,meetNode);
System.out.println("环的连接点为:"+ConnectNode.val);
//(4)带环链表的长度是多少?
int SumLength=sumLength(root,ConnectNode);
System.out.println("带环链表的长度为:"+SumLength);
}
2.KMP算法
package com.company;
public class Main {
public static void main(String[] args) {
String text =new String("aaacaaab");
String pattern=new String("aaab");
int location=KMPMatch(text,pattern);
System.out.println("\n"+location);
}
public static int[] getNextArray(String pattern){
int[] next=new int[pattern.length()];
int k=-1;
next[0]=-1;
int j=0;
char[] patternArray=pattern.toCharArray();
while(j<pattern.length()-1){
if(k==-1||patternArray[j]==patternArray[k]){
if(patternArray[++j]==patternArray[++k]){
next[j]=next[k];
}
}
else{
k=next[k];
}
}
return next;
}
public static int KMPMatch(String text,String pattern){
int[] next=getNextArray(pattern);
for(int x:next){
System.out.print(x+" ");
}
int j=0;
int i=0;
while(i<text.length()&&j<pattern.length()){
if(j==-1||text.charAt(i)==pattern.charAt(j)){
j++;
i++;
}
else{
j=next[j];
}
}
if(j==pattern.length()){
return i-j;
}
return -1;
}
public static boolean BFMatch(String text,String pattern) {
boolean flag = false;
int j;
for (int i = 0; i < text.length() - pattern.length() + 1; i++) {
for (j = 0; j < pattern.length(); j++) {
if (text.charAt(i + j) != pattern.charAt(j)) {
break;
}
}
if (j == pattern.length()) {
flag = true;
System.out.println("匹配成功!");
break;
}
}
return flag;
}
}
2.二叉树(树、森林)
(1)二叉树的存储:链表最常见、数组也可以(数组的话孩子结点和父节点存在关系,但是必须是在完全二叉树的基础上才有的,父节点存储位置为n时,子节点为2n和2n+1)
(2)二叉树的遍历:先序遍历、中序遍历、后序遍历,一般采用递归的形式,三者不同的就是当前结点的输出语句的位置,当然,也可以采用迭代的方式,可以使用栈,在这里先序遍历、中序遍历比较好弄,但是后续比较难。
1)中序遍历非递归
public void FirstSearch(){ //非递归式
TreeNode root=this;
Stack<TreeNode> stack=new Stack<>();
while (root!=null||!stack.empty()){
if(root!=null){
stack.push(root);
root=root.left;
}else{
TreeNode temp=stack.pop();
System.out.print(temp.val+" ");
root=temp.right;
}
}
}
2)先序遍历非递归
public void FirstSearch(){ //非递归式
TreeNode root=this;
Stack<TreeNode> stack=new Stack<>();
while (root!=null||!stack.empty()){
if(root!=null){
System.out.print(root.val+" ");
stack.push(root);
root=root.left;
}else{
TreeNode temp=stack.pop();
root=temp.right;
}
}
}
==1)二叉树又有:二叉排序树、平衡二叉树、红黑树等等,都是为了查询更加迅速而产生的,因为二叉排序树有缺点(即线性结构,查询复杂度为O(n))所以产生了平衡二叉树和红黑树。=
==2)平衡二叉树和红黑树的区别:红黑树是弱的平衡二叉树,它的条件最终只是显示红黑树中不存在路径超过其他路径的两倍长度,而平衡二叉树定义了非常严格的平衡因子,限制平衡因子(|左子树的高度-右子树的高度|)<=1,所以平衡二叉树要比红黑树矮一些,对于查找更加快速,但是对于插入和删除要付出的更多,所以一般应用更多的是更实用的、不那么严格的红黑树。=
==3)红黑树的标准(红黑树的基础是二叉搜索树):
1.结点要么是红色的要么是黑色的。
2.根节点是黑色的。
3.每个红结点的孩子结点均为黑色结点。(即不存在连续的红结点)
4.叶子结点是空的黑结点。
5.对于每个结点来说,其到叶子结点的路径所具有的黑色结点的数目均相同。
6.新加入红黑树的结点为红色结点
红黑树的调节:如果加入后不破坏那么不用变,否则先变色,变完色后如果还不行那么就需要在进行旋转。
3.图
(1)存储方式:邻接表、邻接矩阵,存储方式不同之后的算法的时间复杂度也会不同。
(2)遍历:深度优先,这里要考虑连不连通。广度优先遍历。因为图的逻辑结构是多对多,所以需要在这里用一个数组S[]表示该点是否被访问过,遍历类似树的先序遍历和层次遍历。还有有就是,对于邻接表这里的遍历的时间复杂度是O(n+e),邻接矩阵是O(n^2)。
(3)应用:
1)最小生成树:普利姆算法(Prim)和克鲁斯卡尔(Kruskal)。
一个是加点,需要增加closedge[]结构,在加入的过程中需要更新closedge[]数组的值,数组中记录了更跟该点(还未加入)的点相连接的(已加入的)点的最小距离以及点。遍历vexnum-1次即可加入所有的点,加入的点要设置其closedge[i].lowcost=0
一个是加边,需要增加额外的edge[][]数组表示加入的边的结点以及权重,另外需要一个VexSet[]用于记录连接分量值,直接对所有的边进行排序,然后遍历所有的边即可。注意加入边之后要修改连通分量一致。
2)最短路径:迪杰斯特拉(求该点到其余所有点之间的最短距离)(O(n^2))、弗洛伊德(求两点之间的最短距离)O(n ^3))。
迪杰斯特拉:注意迪杰斯特拉跟之前的加点的那个Prim算法不同之处,Prim算法也是用D存储最短的路径,但是他的这个最短路径在该点加入之后就会更新为0,所以可以代替S[]数组判断该点是否加入的作用,但是迪杰斯特拉这里是某个点到其余所有点的距离,他不会更新为0的,所以需要额外的数组D[]去记录该点是否已经被计算过。主循环n-1次即可,有path记录path[k]表示结点v–>k的最短路径从k走最邻近的,遍历之后也可以将路径打印出来。
弗洛伊德:进行三重循环,将每个点分别加入所有的边之间判断是否距离更短,即:if(D[i][k]+D[k][j]<D[i][j]),并且记录相应的path值,即path[i][j]为j到i直接相连的前驱,顺次往下找可以找到所有经过的点。
4)拓扑排序:会算即可,表示从活动开始到活动结束的最长路径。
4.所有排序的总结:
(1)排序有:插入排序:直接插入排序、折半插入排序、希尔排序(增量排序)。
排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场合 |
---|---|---|---|---|
直接插入排序 | O(n^2 | O(1) | 稳定 | 适合n的数目不太大,较为有序 |
折半插入排序 | O(n^2) | O(1) | 稳定 | 减少查询次数,适合初始记录无序,n较大 |
希尔排序(缩小增量排序) | O(n^3/2–>nlog(n) ^2) | O(1) | 不稳定 | n越大越好,适合无序 |
冒泡排序 | O(1)–>O(n^2) | O(1) | 稳定 | 适合初始基本有序的,n较小 |
快速排序 | O(n*log(n)) | O(log(n))–>O(n) | 不稳定 | 递归需要空间,适合n很大,很无序的 |
简单选择排序 | O(n^2) | O(1) | 不稳定 | 跟冒泡排序比移动次数减少,适合每记录占用的存储空间较多 |
堆排序 | 最坏O(n*log2(n)) | O(1) | 不稳定 | 非常不适合n小,因为建堆费时间,适合n大且无序 |
归并排序 | O(n*log(n)) | O(n) | 稳定 | 可用于链式 |
基数排序 | O(n)–>O(nlog(n)) | O(n+r*d) | 稳定 | 多关键字排序 |
外部排序(基于归并排序) | 主要是IO操作费时 | 利用内存可以占用的空间 | 稳定 | 在内存空间不足时使用的多 |
(2)堆排序:
可以看做是一个完全二叉树,存储只用顺序结构(数组)以实现随机查找。因为是完全二叉树所以结点n的双亲为n/2(想下取整)
堆排序结构跟之间的树最不一样的就是,虽然是两个孩子但这里的孩子并不区分左右,即大根堆就是双亲结点比孩子结点的值大,小根堆就是双亲结点比孩子结点的值小!!!
1)涉及到:筛选法调整堆、建立初堆(其实就是从结点n/2到结点1依次调用调整堆算法。)、堆排序(每次都输出根结点,之后将根节点与最后一个结点交换,空间n-1,然后以根节点为树去调整堆。)
最坏情况下差不多也是O(n*log(n)),比起快速排序最坏情况下O(n^2)而言是有点。
5.所有查找的总结:
(1)大类有:顺序查找(任何存储结构的线性表)、折半查找(有序的顺序表)、分块查找、(树形查找):二叉排序树、二叉平衡树、B-树、B+树、红黑树、散列表(hash表)
(2)顺序查找:链式和顺序表均适合,时间复杂度为O(n)。
(3)折半查找:必须有序而且顺序存储(要求较为严格)。时间复杂度为O(logn)
(4)分快查找:块内无序,块间有序。先块间查询,再块内查询,一般块间可以采用折半查询,块内只能采用顺序查找的方法。复杂度要根据块间如何查找来区分。
假设有n个数,每块大约d个,即大概有n/d块,块间顺序查找大概平均时间复杂度为O(1/2(d+n/d)+1),快间折半平均时间复杂度为:O(1/2*d+log(n/d+1))。
(5)树表的查找:
1)二叉排序树:
定义:每个结点的左子树的值均小于该节点的值,右子树结点的值均大于该节点的值,不存在重复值。
==涉及二叉排序树的插入,查找,建立(其实就是查找,查找之后找得到的一定是叶子结点,然后插入即可。),删除(删除就涉及的比较多了,分为删除的结点没孩子、有一个孩子、有两个孩子)
2)平衡二叉树:在二叉排序树的基础上增加了平衡因子=该节点左子树的高度-右子树的高度,平衡因子规定只能为-1、0 、1
==查询和二叉排序树一样,但是插入第一步和二叉排序树一样,但是之后可能会涉及调整,LL、RR、LR、RL四种类型的调整。删除涉及删除结点孩子的数目,相应的进行删除。
3)B-树:
==m阶B-树,每个结点最多m个子树,m-1个关键字,除根节点外每个节点最少m/2(向上取整)个子树,m/2向上取整)-1个关键字,每个节点关键字左部的子树上的所有关键字均小于该关键字,右部均大于该关键字。
4)B+树:
m阶B+树,每个结点最多m个子树,m个关键字,除根节点外每个节点最少m/2(向上取整)个子树,m/2向上取整)个关键字,每个节点关键字左部的子树上的所有关键字均小于该关键字,每个关键字对应一个子树,除了叶子结点之外的结点的值都是索引,叶子结点保存了所有的值,而且所有的叶子结点都用一张链表存储起来了
B+树比B-树好在,如果需要查询一定范围内的数,B-树需要中序遍历查询直到查完所需要的结点,但是B+树只需要中序遍历查到第一个结点即可利用链表的结构直接往后查,不再需要中中序遍历。
5)红黑树:略
(6)Hash表(散列表):
1)遇到冲突的方法:
开放地址法:线性探测法、二次探测法、伪随机探测法。链地址法:利用链表去存储。
常考题目:平均查找成功的长度—>需要自己构造看这些数据存储时比较了多少次。
平均查找失败的长度—>是需要查看散列函数的r,即通过散列函数我求得的值有多少个作为分母,分别以这些数去看查询为空时即查询失败的次数。