二叉树的层次遍历
例题:
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
使用一个队列,来保存每一层的结点。每次我们从队列中依次取出这一层的所有结点,然后保存它们的值为一个列表,并将它们的左右结点入队。
当队列为空的时候,二叉树也就被遍历完了。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
Queue<TreeNode> queue = new LinkedList<TreeNode>(); //用一个队列来存储每一层的节点
if(root!=null) queue.offer(root);
while(queue.size()>0){
List<Integer> list = new ArrayList<Integer>();
int length = queue.size();
for(int i=0;i<length;i++){
TreeNode t = queue.poll();
list.add(t.val);
if(t.left!=null) queue.offer(t.left);
if(t.right!=null) queue.offer(t.right);
}
res.add(list);
}
return res;
}
}
其实,这个题也可以用DFS的方法来做,只是不再使用队列,但这里也写一下。
- 使用递归的方式,在每个递归方法中都会传入一个值
level
(从0开始)。 - level的作用是用来标注现在是第几层,然后在自己对应层的List中添加当前结点的值。
- 如果
res.size()==level
,说明当前层第一次被遍历,则会在res中创建一个新list。
class Solution {
LinkedList<List<Integer>> res = new LinkedList<List<Integer>>();
public List<List<Integer>> levelOrder(TreeNode root) {
if(root!=null) findNextNode(root,0);
return res;
}
public void findNextNode(TreeNode root,int level){
List<Integer> list = null;
if(res.size()==level){
list = new LinkedList<Integer>();
res.add(list);
}else{
list = res.get(level);
}
list.add(root.val);
if(root.left!=null) findNextNode(root.left,level+1);
if(root.right!=null) findNextNode(root.right,level+1);
}
}
1. 二叉树的层次遍历 II
给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
最简单的方法,就是先按照例题的方式得到从上到下的然后再反转List即可。反转的方式有很多种,这里采用最简单的头插法插入List。
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
LinkedList<List<Integer>> res = new LinkedList<List<Integer>>();
Queue<TreeNode> queue = new LinkedList<TreeNode>(); //用一个队列来存储每一层的节点
if(root!=null) queue.offer(root);
while(queue.size()>0){
List<Integer> list = new ArrayList<Integer>();
int length = queue.size();
for(int i=0;i<length;i++){
TreeNode t = queue.poll();
list.add(t.val);
if(t.left!=null) queue.offer(t.left);
if(t.right!=null) queue.offer(t.right);
}
res.addFirst(list);
}
return res;
}
}
2.二叉树的锯齿形层次遍历
给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
初步思路(BFS):
- 设置一个标志位,表示当前遍历的层是奇数层还是偶数层;
- 根据上一题的经验可以得知,不需要对队列做什么改变,只需要在插入当前层的List时如果是奇数层就尾插,偶数层就头插即可。
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
boolean level = false; //false表示奇数层,true表示偶数层
Queue<TreeNode> queue = new LinkedList<TreeNode>();
LinkedList<List<Integer>> res = new LinkedList<List<Integer>>();
if(root!=null) queue.offer(root);
while(!queue.isEmpty()){
int length = queue.size();
LinkedList<Integer> list = new LinkedList<Integer>();
for(int i=0;i<length;i++){
TreeNode t = queue.poll();
if(t.left!=null) queue.offer(t.left);
if(t.right!=null) queue.offer(t.right);
if(level){
list.addFirst(t.val);
}else{
list.add(t.val);
}
}
res.add(list);
level = !level;
}
return res;
}
}
3.二叉树的右视图
给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
跟当初字节的面试题有点像,不过更简单。只需要打印出最右边的结点就好了,所以使用一个队列,每次都打印出当前层最后一个即可:
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> list = new ArrayList<Integer>();
Queue<TreeNode> queue = new LinkedList<TreeNode>(); //用一个队列来存储每一层的节点
if(root!=null) queue.offer(root);
while(queue.size()>0){
int length = queue.size();
for(int i=0;i<length;i++){
TreeNode t = queue.poll();
if(i==length-1) list.add(t.val);
if(t.left!=null) queue.offer(t.left);
if(t.right!=null) queue.offer(t.right);
}
}
return list;
}
}
BFS和图的最短路径
1. 完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
乍一看,这个题和图毫无关系,但是,实际上却是一个无权图的最短路径问题:
- 从n到0,每一个数字表示一个节点
- 如果两个数字x到y之间相差一个完全平方数,则连接一条边
- 我们得到了一个无权图
- 原问题转化成,求这个无权图中从n到0的最短路径
class Solution {
public int numSquares(int n) {
Queue<Integer> queue = new LinkedList<Integer>();
boolean[] visit = new boolean[n+1]; //标志是否被访问
for(boolean v:visit) v = false;
visit[n] = true;
queue.offer(n);
int res = 0;
while(!queue.isEmpty()){
res++;
int length = queue.size();
for(int i=0;i<length;i++){
int num = queue.poll();
for(int j=1;num-j*j>=0;j++){
if(num-j*j==0) return res;
if(!visit[num-j*j]){
queue.offer(num-j*j);
visit[num-j*j] = true;
}
}
}
}
return res;
}
}
2.单词接龙
给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord
的最短转换序列的长度。转换需遵循如下规则:每次转换只能改变一个字母。 转换过程中的中间单词必须是字典中的单词。
说明:
-如果不存在这样的转换序列,返回 0。
-所有单词具有相同的长度。
-所有单词只由小写字母组成。
-字典中不存在重复的单词。
-你可以假设beginWord 和 endWord 是非空的,且二者不相同。
和上一题相似的原理,这个题也可以转化为无权图求最短路径。
- 从beginWord到endWord,每一个单词代表一个节点
- 如果两个单词只相差一个字母,则两个节点之间有一条边
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
if(!wordList.contains(endWord)) return 0; //当字典中没有endword直接返回0
boolean[] visit = new boolean[wordList.size()]; //标志是否被访问
for(int i=0;i<visit.length;i++) visit[i]=false;
Queue<String> queue = new LinkedList<String>();
int level = 1; //标志最短路径长度
queue.offer(beginWord);
while(!queue.isEmpty()){
level++;
int length = queue.size();
for(int i=0;i<length;i++){
String s = queue.poll();
if(canTransfer(s,endWord)) return level; //当可W以转化至endWord时,返回长度
for(int j=0;j<wordList.size();j++){
if(!visit[j] && canTransfer(s,wordList.get(j))){
visit[j] = true;
queue.offer(wordList.get(j));
}
}
}
}
return 0;
}
//用来判断是否可以转化
public boolean canTransfer(String a,String b){
int count = 0;
for(int i=0;i<a.length();i++){
if(a.charAt(i)!=b.charAt(i)){
count++;
if(count>1) return false;
}
}
return true;
}
}
此外,这个解法目前复杂度还比较高,可以使用双向遍历来优化,也就是从beginWord到endWord,再从endWord到beginWord。当然,单纯的这样耗时会更长,应该是每次哪个方向的当前层包含节点少遍历哪一个方向。
优先队列
例题:前 K 个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
最简单的思路是遍历一遍数组,然后统计每个元素的个数,排序后找到前k个元素。
另一个思路则是使用:优先队列。
维护一个含有k个元素的优先队列。当队列满的时候,如果遍历到的元素比队列中的最小频率元素的频率高,则取出队列中最小频率的元素,将新元素入队。最终,队列中剩下的,就是前k个出现频率最高的元素。
class Solution {
public List<Integer> topKFrequent(int[] nums, int k) {
HashMap<Integer,Integer> map = new HashMap<>(); //用来统计每个数字的出现次数
PriorityQueue<Integer> queue = new PriorityQueue<Integer>((n1, n2) -> map.get(n1) - map.get(n2));
for (int num : nums) {
map.put(num,map.getOrDefault(num,0)+1);
}
for (Integer n : map.keySet()) {
queue.offer(n);
if(queue.size()>k) queue.poll();
}
List<Integer> list = new ArrayList<>();
while(!queue.isEmpty()) list.add(queue.poll());
return list;
}
}
1.合并K个排序链表
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
这个怎么看也不像是可以用优先队列解决的,看了看题解发现,原来如此:
- 维护一个长度为k的优先队列,存储k个链表的头结点
- 每次出队最小的结点,然后挂在新链表上,同时将它的下一个结点入队。
- 最后直到队列为空,新的链表也就建好了。
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> queue = new PriorityQueue<ListNode>((n1,n2) -> n1.val-n2.val);
ListNode newList = new ListNode(0);
ListNode cur = newList;
for (ListNode node : lists) {
if(node!=null) queue.offer(node);
}
while(!queue.isEmpty()){
ListNode node = queue.poll();
cur.next = node;
cur = cur.next;
if(node.next!=null) queue.offer(node.next);
}
return newList.next;
}
}