「左程云-算法与数据结构笔记」| P7 二叉树

518 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第五天,点击查看活动详情。

最近在看左神的数据结构与算法,考虑到视频讲的内容和给出的资料的PDF有些出入,不方便去复习,打算出一个左神的数据结构与算法的笔记系列供大家复习,同时也可以加深自己对于这些知识的掌握,该系列以视频的集数为分隔,今天是第二篇:P7|二叉树


前两天去珠海旅游看海啦嘿嘿嘿,所以断更了两天,现在重新回来继续更啦

珠海.jpg

一、判断一个链表是否成环

本题除了要判断该链表是否成环,还要返回入环结点,下面将分解来讲

判断链表是否成环

  • 判断链表是否成环用到的方法是:快慢指针:
    • 每当慢指针slow走一步,快指针fast走两步
  • 根据二者的相遇情况来判断:
    • 如果二者相遇了,代表该链表成环。因为快指针和慢指针都会进入环中,快指针一次走两步,总会从后面多绕几圈追上慢指针。
    • 如果二者没有相遇,即快指针遍历结束了,指向了 null。自然就链表没有成环。

返回入环结点

此处左神没有讲实现的思路,只是给了个结论,鄙人比较好奇,就去搜了一下,然后看到了 labuladong 对这个的讲解

  • 在快指针追上慢指针的时候,二者的路程差,一定是环的长度的整数倍
  • 现假设:慢指针走了 k 步,那么快指针走了 2k 步,且假设相遇点到入环接点的距离为m

单链表的入环结点(图二).jpg

单链表的入环结点(图一).jpg

  • 可以发现:
    • 快指针再次到达环起点的路程为: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 指向了两个结点
  • 那么只可能是:两个链表的最后一部分是相同的

image.png

实现思路

  • 首先分别遍历链表,找到各自的尾结点,并且统计链表的长度
  • 比较两个链表的最后一个结点的内存地址是否相等
    • 如果不相等,就代表两个单链表并不相交
    • 如果相等,代表此时两个单链表相交,但是要找到相交的头结点
  • 因此就有:让链表长度更大的那个链表先走到和另外一个链表相同长度
    • 比如说一个链表长度为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、如果有一个链表成环

  • 如果一个链表有环,另一个链表无环,二者不可能相交 image.png
  • 不难发现,不论从哪里插入一个与这个链表相交的链表,这个插入的链表也会变成一个成环的链表

3、如果两个链表都有环

分析

  1. 对于两个成环的链表,可能的情况只有三种:

image.png

  1. 对于 Situation1,两个链表的入环结点一样
  2. 对于 Situation2,两个链表的入环结点不一样
  3. 对于 situation3,两个链表不相交
  4. 首要步骤是判断两个链表是否成环,如果成环我们是可以得到它的头结点的,即入环结点,比较两个入环结点是否一样,如果一样进 situation 1,如果不一样进 situation 2situation 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、递归方法遍历树

递归序

  • 对于一颗二叉树: image.png
  • 我们遍历进行输出的时候,是需要使用到递归的,根节点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、非递归方法打印树

先序遍历

image.png

  • 首先根节点压栈
  • 然后弹栈并打印该结点
  • 然后如果有孩子的话,就先右孩子然后左孩子压栈
  • 然后依次类推
	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();  
	}
  • 首先把根节点的每颗子树的左子树压栈
  • 然后在弹栈的时候做判断:判断该结点是否为空。
  • 结点为空的情况:
    • 依次把子树的左子树压栈的时候,最后一颗子树
    • 在弹栈的时候,该节点没有了右孩子
  • 如果为空,就输出,然后找下一个右孩子
  • 如果不为空,就继续把左孩子压栈,此时如果没有左孩子,下一次循环就进入了弹栈的条件
  • 这个听起来可能有点乱,我们不妨举个例子来看看 image.png
  • 多去结合代码去看,图片只是梳理一下流程,帮助理解

后序遍历

  • 我们已知:
    • 先序遍历是:先头然后左然后右
    • 后序遍历是:先左然后右然后头
  • 我们可以这么想:在进行非递归先序遍历的时候,是采用先右孩子压栈再左孩子压栈的方式,最后的结果是:头、左、右
  • 现我们先左孩子压栈然后右孩子压栈,得到的应该就是:头、右、左
  • 而后续遍历需要的是:左、右、头
  • 因此我们可以利用栈的特性,把修改后的“先序遍历”压栈,然后依次弹栈就能得到后续遍历
	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

image.png

实现思路

  • 准备一个队列,首先放根节点,然后在出队列的时候,先放左后放右 image.png

代码实现

	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;  
	}