持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第五天,点击查看活动详情。
最近在看左神的数据结构与算法,考虑到视频讲的内容和给出的资料的PDF有些出入,不方便去复习,打算出一个左神的数据结构与算法的笔记系列供大家复习,同时也可以加深自己对于这些知识的掌握,该系列以视频的集数为分隔,今天是第二篇:P7|二叉树
前两天去珠海旅游看海啦嘿嘿嘿,所以断更了两天,现在重新回来继续更啦
一、判断一个链表是否成环
本题除了要判断该链表是否成环,还要返回入环结点,下面将分解来讲
判断链表是否成环
- 判断链表是否成环用到的方法是:快慢指针:
- 每当慢指针
slow走一步,快指针fast走两步
- 每当慢指针
- 根据二者的相遇情况来判断:
- 如果二者相遇了,代表该链表成环。因为快指针和慢指针都会进入环中,快指针一次走两步,总会从后面多绕几圈追上慢指针。
- 如果二者没有相遇,即快指针遍历结束了,指向了 null。自然就链表没有成环。
返回入环结点
此处左神没有讲实现的思路,只是给了个结论,鄙人比较好奇,就去搜了一下,然后看到了 labuladong 对这个的讲解
- 在快指针追上慢指针的时候,二者的路程差,一定是环的长度的整数倍
- 现假设:慢指针走了
k步,那么快指针走了2k步,且假设相遇点到入环接点的距离为m
- 可以发现:
- 快指针再次到达环起点的路程为:
k-m - 头结点再次到达环起点的路程为:
k-m
- 快指针再次到达环起点的路程为:
- 因此让慢指针从头开始遍历,快指针减速,每次只走一步,二者就能在入环结点相遇
实现代码
public static Node getLoopNode(Node head) {
if (head == null || head.next == null || head.next.next == null) {
return null;
}
Node n1 = head.next; // n1 -> slow
Node n2 = head.next.next; // n2 -> fast
while (n1 != n2) {
if (n2.next == null || n2.next.next == null) {
return null;
}
n2 = n2.next.next;
n1 = n1.next;
}
n2 = head; // n2 -> walk again from head
while (n1 != n2) {
n1 = n1.next;
n2 = n2.next;
}
return n1;
}
二、两个单链表相交
题目:给定两个可能有环也可能无环的单链表,头结点head1和头结点head2。请实现一个函数,如果两个链表相交,则返回相交的第一个结点,如果不相交,返回null
1、如果两个链表都不成环
分析
- 首先不可能是 x 型相交,因为 x 型相交的前提是,有一个指针的 next 指向了两个结点
- 那么只可能是:两个链表的最后一部分是相同的
实现思路
- 首先分别遍历链表,找到各自的尾结点,并且统计链表的长度
- 比较两个链表的最后一个结点的内存地址是否相等
- 如果不相等,就代表两个单链表并不相交
- 如果相等,代表此时两个单链表相交,但是要找到相交的头结点
- 因此就有:让链表长度更大的那个链表先走到和另外一个链表相同长度
- 比如说一个链表长度为10,另一个链表长度为8,那么就让长度为10的链表先走2步
- 然后两个指针,一起向下遍历,直到两个指针指向的结点地址相等,此时就是相交的第一个结点
实现代码
public static Node noLoop(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node cur1 = head1;
Node cur2 = head2;
int n = 0;
while (cur1.next != null) {
n++;
cur1 = cur1.next;
}
while (cur2.next != null) {
n--;
cur2 = cur2.next;
}
if (cur1 != cur2) {
return null;
}
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
n = Math.abs(n);
while (n != 0) {
n--;
cur1 = cur1.next;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
2、如果有一个链表成环
- 如果一个链表有环,另一个链表无环,二者不可能相交
- 不难发现,不论从哪里插入一个与这个链表相交的链表,这个插入的链表也会变成一个成环的链表
3、如果两个链表都有环
分析
- 对于两个成环的链表,可能的情况只有三种:
- 对于
Situation1,两个链表的入环结点一样 - 对于
Situation2,两个链表的入环结点不一样 - 对于
situation3,两个链表不相交 - 首要步骤是判断两个链表是否成环,如果成环我们是可以得到它的头结点的,即入环结点,比较两个入环结点是否一样,如果一样进
situation 1,如果不一样进situation 2和situation 3
situation 1
- 在知道入环结点一直后,我们的处理方法就和
两个不成环的链表的处理方法一致了,先让长的走差值,然后两个一起走,走到相同的地方就是相交的结点
situation 2 & situation 3
- 其中一个入环结点不动,另外一个入环结点遍历一圈,直到遇到自己
- 如果在此期间遇到了另一个环的入环结点,就是 situation2 ,那么这两个入环结点都是二者相交的结点
- 但是遍历一圈直到转回自己都没遇到,那就是 situation3
实现代码
public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
Node cur1 = null;
Node cur2 = null;
if (loop1 == loop2) {
cur1 = head1;
cur2 = head2;
int n = 0;
while (cur1 != loop1) {
n++;
cur1 = cur1.next;
}
while (cur2 != loop2) {
n--;
cur2 = cur2.next;
}
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
n = Math.abs(n);
while (n != 0) {
n--;
cur1 = cur1.next;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
} else {
cur1 = loop1.next;
while (cur1 != loop1) {
if (cur1 == loop2) {
return loop1;
}
cur1 = cur1.next;
}
return null;
}
}
4、代码实现
public static Node getIntersectNode(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node loop1 = getLoopNode(head1);
Node loop2 = getLoopNode(head2);
if (loop1 == null && loop2 == null) {
return noLoop(head1, head2);
}
if (loop1 != null && loop2 != null) {
return bothLoop(head1, loop1, head2, loop2);
}
return null;
}
- 其中
getLoopNode是判断链表是否成环中的函数 noLoop和bothLoop是上述的情况中的函数
三、树的基本概念
1、递归方法遍历树
递归序
- 对于一颗二叉树:
- 我们遍历进行输出的时候,是需要使用到递归的,根节点1 递归调用 左节点2 和 右节点3
- 其中左节点又递归调用 结点4 和 结点5
- 节点4 左孩子,调用右孩子,然后返回
- 所以递归序就是:1,2,4,4,4,2,5,5,5,2,1,3,6,6,6,3,7,7,7,3,1
先序打印
先序打印:在打印的时候,先打印头结点,左孩子的所有结点,然后右孩子的所有结点- 我们不难发现,它的本质是:递归序第一次出现的时候进行打印,那就是先序打印
中序打印
中序打印:在打印的时候,先打印左节点,然后头结点,最后右结点- 我们不难发现,它的本质是:递归序第二次出现的时候进行打印,那就是中序打印
后序打印
后序打印:打印的时候,先打印左节点,然后右节点,最后头结点- 我们不难发现,它的本质是:递归序第三次出现的时候进行打印,那就是中序打印
实现代码
// 递归方式先序打印
public static void preOrderRecur(Node head) {
if (head == null) {
return;
}
System.out.print(head.value + " ");
preOrderRecur(head.left);
preOrderRecur(head.right);
}
// 递归方式中序打印
public static void inOrderRecur(Node head) {
if (head == null) {
return;
}
inOrderRecur(head.left);
System.out.print(head.value + " ");
inOrderRecur(head.right);
}
// 递归方式后序打印
public static void posOrderRecur(Node head) {
if (head == null) {
return;
}
posOrderRecur(head.left);
posOrderRecur(head.right);
System.out.print(head.value + " ");
}
2、非递归方法打印树
先序遍历
- 首先根节点压栈
- 然后弹栈并打印该结点
- 然后如果有孩子的话,就先右孩子然后左孩子压栈
- 然后依次类推
public static void preOrderUnRecur(Node head) {
System.out.print("pre-order: ");
if (head != null) {
Stack<Node> stack = new Stack<Node>();
stack.add(head);
while (!stack.isEmpty()) {
head = stack.pop();
System.out.print(head.value + " ");
if (head.right != null) {
stack.push(head.right);
}
if (head.left != null) {
stack.push(head.left);
}
}
}
System.out.println();
}
中序遍历
public static void inOrderUnRecur(Node head) {
System.out.print("in-order: ");
if (head != null) {
Stack<Node> stack = new Stack<Node>();
while (!stack.isEmpty() || head != null) {
if (head != null) {
stack.push(head);
head = head.left;
} else {
head = stack.pop();
System.out.print(head.value + " ");
head = head.right;
}
}
}
System.out.println();
}
- 首先把根节点的每颗子树的左子树压栈
- 然后在弹栈的时候做判断:判断该结点是否为空。
- 结点为空的情况:
- 依次把子树的左子树压栈的时候,最后一颗子树
- 在弹栈的时候,该节点没有了右孩子
- 如果为空,就输出,然后找下一个右孩子
- 如果不为空,就继续把左孩子压栈,此时如果没有左孩子,下一次循环就进入了弹栈的条件
- 这个听起来可能有点乱,我们不妨举个例子来看看
- 多去结合代码去看,图片只是梳理一下流程,帮助理解
后序遍历
- 我们已知:
- 先序遍历是:先头然后左然后右
- 后序遍历是:先左然后右然后头
- 我们可以这么想:在进行非递归先序遍历的时候,是采用先右孩子压栈再左孩子压栈的方式,最后的结果是:头、左、右
- 现我们先左孩子压栈然后右孩子压栈,得到的应该就是:头、右、左
- 而后续遍历需要的是:左、右、头
- 因此我们可以利用栈的特性,把修改后的“先序遍历”压栈,然后依次弹栈就能得到后续遍历
public static void posOrderUnRecur1(Node head) {
System.out.print("pos-order: ");
if (head != null) {
Stack<Node> s1 = new Stack<Node>();
Stack<Node> s2 = new Stack<Node>();
s1.push(head);
while (!s1.isEmpty()) {
head = s1.pop();
s2.push(head);
if (head.left != null) {
s1.push(head.left);
}
if (head.right != null) {
s1.push(head.right);
}
}
while (!s2.isEmpty()) {
System.out.print(s2.pop().value + " ");
}
}
System.out.println();
}
3、二叉树的宽度优先遍历
概念
所谓宽度优先遍历,就是按照“宽度”遍历,类似于走楼梯,一层一层走下来,第一次遍历第一层,第二次遍历第二层 比如这棵树的遍历方式就是:1、2、3、4、5、6、7
实现思路
- 准备一个队列,首先放根节点,然后在出队列的时候,先放左后放右
代码实现
public static void widthTraversal(Node head){
if (head == null){
return;
}
Queue<Node> queue = new LinkedList<>();
queue.add(head);
while(!queue.isEmpty()){
Node cur = queue.poll();
System.out.println(cur.value);
if (cur.left != null){
queue.add(cur.left);
}
if (cur.right != null){
queue.add(cur.right);
}
}
}
4、求二叉树的最大宽度
题目
最大宽度实际上就是,哪一层的节点数最多,最多的节点数就是最大宽度
实现思路
- 在上述的宽度优先遍历,把左右孩子放进队列的时候,使用哈希表记录下来,他们所在的层数,同时把当前层节点数加一
- 然后每次一个层数结束就做一个结算,把当前层数的结点个数清零,然后把当前层数加一
代码实现
public static void widthTraversal(Node head){
if (head == null){
return 0;
}
Queue<Node> queue = new LinkedList<>();
queue.add(head);
HashMap<Node,Integer> levelMap = new HashMap<>();
levelMap.put(head, 1);
int curLevel = 1;
int curLevelNodes = 0;
int max = 0;
while(!queue.isEmpty()){
Node cur = queue.poll();
int curNodeLevel = levelMap.get(cur);
if (curNodeLevel == curLevel){
curLevelNodes ++;
}else {
max = Math.max(max, curLevelNodes);
curLevel++;
curLevelNodes = 1;
}
if (cur.left != null){
levelMap.put(cur.left,curNodeLevel+1);
queue.add(cur.left);
}
if (cur.right != null){
levelMap.put(cur.right,curNodeLevel+1);
queue.add(cur.right);
}
}
return max;
}