刷题笔记:二叉树与图专题(第一遍刷搞懂每道题的解法;第二遍刷搞懂这一类题通用的思想和策略;第三遍刷思考什么样的题属于这个类别)

1.二叉树相关基础

1.1二叉树的构造

在本刷题笔记中不会介绍概念等基础知识,更多记录代码演示等与算法编码能力直接相关的内容。

首先看回顾一下二叉树数据结构的代码实现:

struct TreeNode {
	TreeNode * left;
	TreeNode * right;
	int value;
	//使用对象初始化列表进行初始化
	TreeNode(int x):value(x),left(NULL),right(NULL){}
};

1.2二叉树的遍历

前序遍历:根--->左子树--->右子树

递归写法:

//前序遍历
void PreOrder(TreeNode* node) {
	if (!node) {
		return;
	}
	//访问根
	cout << node->value;
	//左子树
	PreOrder(node->left);
	//右子树
	PreOrder(node->right);
}

非递归写法:

void PreOrder(TreeNode *root) {
	TreeNode * p = root;
	stack<TreeNode*> s;
	while (p || !s.empty())
	{
		if (p) {
			cout << p->value << " ";
			s.push(p);
			p = p->left;
		}
		else {
			p = s.top();
			p = p->right;
			s.pop();
		}
	}
}

中序遍历:左子树--->根--->右子树

递归写法:

//中序遍历
void MidOrder(TreeNode* node) {
	if (!node) {
		return;
	}	
	//左子树
	MidOrder(node->left);
	//访问根
	cout << node->value;
	//右子树
	MidOrder(node->right);
}

非递归写法:

void MidOrder(TreeNode *root) {
	stack<TreeNode*> s;
	TreeNode* p = root;
	while (p || !s.empty())
	{
		//
		if (p) {
			s.push(p);
			p = p->left;
		}
		else{
			p = s.top();
			cout << p->value<<" ";
			p = p->right;
			s.pop();
		}
	}
}

后序遍历:左子树--->右子树--->根

递归写法:

//后序遍历
void PostOrder(TreeNode* node) {
	if (!node) {
		return;
	}
	//左子树
	PostOrder(node->left);
	//右子树
	PostOrder(node->right);
	//访问根
	cout << node->value;
}

非递归写法

因为后续非递归遍历的顺序是先访问左子树,再访问右子树,最后访问根节点。当用堆栈进行存储节点时必须分清返回根结点时是从左子树返回的还是从右子树返回的。有两种思路可以解决这个问题。

思路1:使用辅助指针来指向最近访问过的结点。

void PostOrder(TreeNode *root) {
	stack<TreeNode*> s;
	TreeNode* p = root;
	TreeNode* last;
	while (p || !s.empty())
	{
		//走到最左
		if (p) {
			s.push(p);
			p = p->left;
		}
		else //向右
		{
			p = s.top();
			//右子树存在且没被访问
			if (p->right && p->right != last) {
				p = p->right;
				s.push(p);
				//再往左
				p = p->left;
			}
			else {
				//否则表示左右子树都访问过了,可以打印根了
				cout << p->value << " ";
				//记录该点
				last = p;
				s.pop();
				//代表s顶的根结点的左子树全部完毕,重设p
				p = NULL;
			}

		}
	}
}

思路2:在结点中添加标志域,记录是否已被访问。

//思路2:在结点中增加标志域,记录是否已被访问。
void PostOrder2(TreeNode *root) {
	stack<TreeNode*> s;
	TreeNode *p = root;
	set<TreeNode*> isVisit;
	while (p || !s.empty())
	{
		//左子树没被访问则一直左走
		if (p) {
			s.push(p);
			p = p->left;
			//左子树添加到以访问
			isVisit.insert(p);
		}
		else {
			p = s.top();
			//右子树没被访问则右走一步
			if (p->right && isVisit.find(p->right) == isVisit.end()) {
				p = p->right;
				isVisit.insert(p);
			}
			else {
				//左右子树都访问过了,打印
				cout << p->value << " ";
				s.pop();
                //这句的目的是避免把刚刚遍历完的左子树又遍历一遍
				p = NULL;
			}
		}
	}
}

2.二叉树题目练习

例1:路径之和(LeetCode 113-中等

题目:给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。

例子:

给定如下二叉树,以及目标和 sum = 22,

              5
             / \
            4   8
           /   / \
          11  13  4
         /  \    / \
        7    2  5   1
返回:

[
   [5,4,11,2],
   [5,8,4,5]
]

分析:

  • 怎么存储遍历路径上的结点?
  • 从根出发到叶子结点的过程一定是深度搜索,那么具体遍历的方式如何选择?
  • 如何判断一个结点为叶子结点?

算法思路:

  • 从根节点开始深度遍历二叉树,遇到某一结点时,将节点值存储至path栈中,使用path_value累加节点值
  • 当遍历至叶节点时,检查path_value是否和目标一致
  • 在节点及子树都遍历完时,将节点从path栈中弹出,path_value减去对应值

代码展示:

递归:

class Solution {
public:
    vector<vector<int>> pathSum(TreeNode* root, int sum) {
        std::vector<vector<int>> result;
        //路径栈
        std::vector<int> path;
        int path_value=0;
        preorder(root,path_value,sum,path,result);
        return result;
    }
private:
    void preorder(TreeNode *node,int &path_value,int sum,vector<int> &path,vector<vector<int>> &result){
        if(!node){
            return;
        }
        //更新权值,添加到路径中
        path_value+=node->val;
        path.push_back(node->val);
    if(!node->left && !node->right && path_value==sum)
       result.push_back(path);
    preorder(node->left,path_value,sum,path,result);
    preorder(node->right,path_value,sum,path,result);
    //弹出当前节点,减去权值
    path_value-=node->val;
    path.pop_back();
    }
    
};

非递归:

class Solution {
public:
    vector<vector<int>> pathSum(TreeNode* root, int sum) {
	  vector<vector<int>> result; 
      vector<int> path; //分别记录所有满足条件的路径、一条满足条件的路径
	  if (!root) return result;
	  stack<TreeNode*> stk; TreeNode *prev = nullptr;
	  while (root || !stk.empty()) { //模拟系统递归栈
		  //一路向左
          while (root) {
              //入栈、更新剩余和、路径
		  	  stk.push(root); 
              sum -= root->val; 
              path.push_back(root->val); 
			  root = root->left;
		  }
          //不能再左了,拿到父节点准备右子树
     	  root = stk.top(); 
           //满足条件,保存路径
		  if (!root->left && !root->right && sum == 0) {
			  result.push_back(path); 
		  }
          //右结点不存在或已经访问 回溯
		  if (!root->right || root->right == prev) { 
			  //出栈、更新剩余和、路径
              stk.pop(); 
              sum += root->val; 
              path.pop_back(); 
              //标记已访问
			  prev = root; 
              //用于回溯到上一级
			  root = nullptr; 
		  }
		  else { //递归访问右结点
			  root = root->right;
		  }
	   }
	  return result;
    }
};

例2:(LeetCode -中等

题目:

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉树:  root = [3,5,1,6,2,0,8,null,null,7,4]

例子:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。

分析:

  • 两节点的公共祖先一定在从根节点到这两个结点的路径上
  • 两个路径中重合部分离根最远的就是答案

算法思路:

预备知识:求根节点至某一结点路径(深度优先搜索,DFS)

  • 从根结点遍历至该节点
  • 遍历过程中按照顺序把路径存储起来
void DFS(TreeNode *node, TreeNode *search,vector<TreeNode*> &path,vector<TreeNode*> &result,int &finish) {
	if (!node || finish) {
		return;
	}
	//先序遍历时放入path
	path.push_back(node);
	if (node == search) {
		finish = 1;
		result = path;
	}
	DFS(node->left, search, path, result, finish);
	DFS(node->right, search, path, result, finish);
	//结束遍历node时,将node节点弹出
	path.pop_back();
}

代码展示:

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        vector<TreeNode*> path;
        vector<TreeNode*> pathP;
        vector<TreeNode*> pathQ;
        bool isfinished=false;
        DFS(root,p,path,pathP,isfinished);
        //记得清空
        path.clear();
        isfinished=false;
        DFS(root,q,path,pathQ,isfinished);
        int path_len=pathP.size()>pathQ.size()?pathQ.size():pathP.size();
        TreeNode* result=NULL;
        for(int i=0;i<path_len;i++){
            if(pathP[i]==pathQ[i]){
                result=pathP[i];
            }
        }
        return result;
    }
private:
     void DFS(TreeNode* root,TreeNode* goal,std::vector<TreeNode*> &path,std::vector<TreeNode*> &result,bool &finished){
         if(!root || finished){
             return;
         }
         //存入路径
         path.push_back(root);
        //判断是不是目标
        if(root==goal){
            finished=true;
            result=path;
        }
        //遍历左右
        DFS(root->left,goal,path,result,finished);
        DFS(root->right,goal,path,result,finished);
        //弹出
        path.pop_back();
    }
};

例3:二叉树转链表(LeetCode 114-中等

题目:给定一个二叉树,原地将它展开为链表。单链表中节点顺序为二叉树前序遍历顺序

例子:

例如,给定二叉树

    1
   / \
  2   5
 / \   \
3   4   6
将其展开为:

1
 \
  2
   \
    3
     \
      4
       \
        5
         \
          6

分析:重点在于如何就地;也就是单链表仍使用保留树的数据结构,left=NULL,right-next

算法思路:

递归的问题只需要想清楚3件事:

1.我在进入递归前要做什么

2.完成递归后进入下个递归前要做什么

3.完成递归后做的事情
代码展示:

class Solution {
public:
    void flatten(TreeNode* root) {
        TreeNode *last =NULL;
        preorder(root,last);
    }
private:
             //当前的结点  最后一个结点,传引用会传出
void preorder(TreeNode* node,TreeNode*&last){
        if(!node){
            return;
        }
        if(!node->left && !node->right ){
            last=node;
            return;
        }
        TreeNode *left = node->left;
        TreeNode *right = node->right;
        //左右子树最后一个结点
        TreeNode *left_last = NULL;
        TreeNode *right_last = NULL;
        //如果有左子树,递归将左子树转换为单链表
        if(left){
            preorder(left,left_last);
            node->left=NULL;
            node->right=left;
            last=left_last;
        }
        //若有右子树,递归将右子树转换单链表
        if(right){
            preorder(right,right_last);
            if(left_last){
                left_last->right = right;
            }
            last=right_last;
        }
    }
};

 

例4:二叉树的层次遍历(基础知识

//二叉树层次遍历
void BFS(TreeNode * root) {
	std::queue<TreeNode*> q;
	q.push(root);
	while (!q.empty())
	{
		TreeNode * temp = q.front();
		cout << temp->value << endl;
		q.pop();
		if (temp->left) {
			q.push(temp->left);
		}
		if (temp->right) {
			q.push(temp->right);
		}
	}
}

例5:侧面观察二叉树(LeeCode 199-中等

题目:

给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

实例:

输入: [1,2,3,null,5,null,4]
输出: [1, 3, 4]
解释:

   1            <---
 /   \
2     3         <---
 \     \
  5     4       <---

代码:

class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        vector<int> result;
        queue<pair<TreeNode*,int>> q;
        if(root){
            q.push(std::make_pair(root,0));
        }
        while(!q.empty()){
            TreeNode *node = q.front().first;
            int depth = q.front().second;
            q.pop();
            if(result.size()==depth){
                result.push_back(node->val);
            }
            else{
                result[depth]=node->val;
            }
            if(node->left){
                q.push(make_pair(node->left,depth+1));
            }
            if(node->right){
                q.push(make_pair(node->right,depth+1));
            }
        }
        return result;
    }
};

3.图相关基础

3.1邻接矩阵

一般带权图、最小生成树、最短路径等算法在邻接矩阵上跑

//临界矩阵
void GraphTest() {
	const int MAX_N = 5;
	int Graph[MAX_N][MAX_N] = { 0 };
	Graph[0][2] = 1;
	Graph[0][4] = 1;
	Graph[1][0] = 1;
	Graph[1][2] = 1;
	Graph[2][3] = 1;
	Graph[3][4] = 1;
	Graph[4][3] = 1;
	for (int i = 0; i < MAX_N; i++) {
		for (int j = 0; j < MAX_N; j++) {
			printf("%d", Graph[i][j]);
		}
		printf("\n");
	}
}

3.2邻接表

//邻接表
struct GraphNode{
	//图的顶点值
	int label;
	//相邻节点指针数组
	std::vector<GraphNode*> neighbors;
	GraphNode(int x) :label(x) {};
};

int main() {
	const int MAX_H = 5;
	//5个顶点
	GraphNode *Graph[MAX_H];

	for (int i = 0; i < MAX_H; i++) {
		Graph[i] = new GraphNode(i);
	}
	//添加边
	Graph[0]->neighbors.push_back(Graph[2]);
	Graph[0]->neighbors.push_back(Graph[4]);
	Graph[1]->neighbors.push_back(Graph[0]);
	Graph[1]->neighbors.push_back(Graph[2]);
	Graph[2]->neighbors.push_back(Graph[3]);
	Graph[3]->neighbors.push_back(Graph[4]);
	Graph[4]->neighbors.push_back(Graph[3]);
	for (int i = 0; i < MAX_H; i++) {
		printf("Label(%d):", i);
		for (int j = 0; j < Graph[i]->neighbors.size(); j++) {
			printf("%d", Graph[i]->neighbors[j]->label);
		}
	}
	for (int i = 0; i < MAX_H; i++) {
		delete Graph[i];
	}

	return 0;
}

3.3图的深度优先遍历

void DFS_Traverse(GraphNode* Graph[]) {
	const int MAX_H = 5;
	int visit[MAX_H] = { 0 };
	for (int i = 0; i < MAX_H; i++) {
		if (visit[i] == 0) {
			DFS_graph(Graph[i], visit);
		}
	}
}

void DFS_graph(GraphNode *node, int visit[]) {
	visit[node->label] = 1;
	printf("%d", node->label);
	for (int i = 0; i < node->neighbors.size(); i++) {
		if (visit[node->neighbors[i]->label] == 0) {
			DFS_graph(node->neighbors[i], visit);
		}
	}
}

3.4图的宽度优先遍历

//广度优先搜索
void BFS_graph(GraphNode* node, int visit[]) {
	std::queue<GraphNode*> q;
	q.push(node);
	visit[node->label] = 1;
	while (!q.empty())
	{
		GraphNode * node = q.front();
		q.pop();
		printf("%d", node->label);
		for (int i = 0; i < node->neighbors.size(); i++) {
			if (visit[node->neighbors[i]->label] == 0) {
				q.push(node->neighbors[i]);
				visit[node->neighbors[i]->label] = 1;
			}
		}
	}
}
void BFS_graph(GraphNode *node, int visit[]) {
	visit[node->label] = 1;
	printf("%d", node->label);
	for (int i = 0; i < node->neighbors.size(); i++) {
		if (visit[node->neighbors[i]->label] == 0) {
			BFS_graph(node->neighbors[i], visit);
		}
	}
}

4.图例题

例1:课程安排(LeetCode 207-中等

题目:

你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。

在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]

给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?

示例:

输入: 2, [[1,0]] 
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。

分析(方法1):

进行深度优先搜索时,如果正在搜索某一顶点(还未退出该顶点得递归深度搜索)时又回到了该点,则证明图有环!

class Solution {
public:
    struct GraphNode
    {
        int val;
        vector<GraphNode*> neighbers;
        GraphNode(int x):val(x){};
    };

    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
            vector<GraphNode*> graph;
            vector<int> visit;//节点访问状态,-1没有访问,0表示正在访问,1表示访问完成
            for(int i=0;i<numCourses;i++){
                //创建图得节点,并赋访问状态为空
                graph.push_back(new GraphNode(i));
                visit.push_back(-1);
            }
            for(int i=0;i<prerequisites.size();i++){
                GraphNode * from = graph[prerequisites[i][1]];
                GraphNode * to = graph[prerequisites[i][0]];
                from->neighbers.push_back(to);
            }
            for(int i=0;i<graph.size();i++){
                if(visit[i]==-1 && !DFS(graph[i],visit)){
                    return false;
                }
            }
            for(int i=0;i<numCourses;i++){
                delete graph[i];
            }
            return true;
    }

    bool DFS(GraphNode * root ,vector<int> & visit){
        visit[root->val]=0;
        for(int i=0;i<root->neighbers.size();i++){
            if(visit[root->neighbers[i]->val]==0){
                return false;
            }
            else if(visit[root->neighbers[i]->val]==-1){
                if(!DFS(root->neighbers[i],visit)){
                    return false;
                }
            }
        }
        visit[root->val]=1;
        return true;
    }

};

分析(方法2):

拓扑排序

class Solution {
public:
    struct GraphNode
    {
        int val;
        vector<GraphNode*> neighbers;
        GraphNode(int x):val(x){};
    };

    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
           vector<GraphNode*> graph;
           //入度数组
           vector<int> degree;
           for(int i=0;i<numCourses;i++){
               degree.push_back(0);
               graph.push_back(new GraphNode(i));
           }
           //初始化,并计算入度
           for(int i=0;i<prerequisites.size();i++){
               GraphNode *from=graph[prerequisites[i][1]];
               GraphNode *to = graph[prerequisites[i][0]];
               from->neighbers.push_back(to);
               degree[to->val]++;
           }
           std::queue<GraphNode*> q;
           for(int i=0;i<numCourses;i++){
                if(degree[i]==0){
                    q.push(graph[i]);
                }
           }
           while(!q.empty()){
               GraphNode* node = q.front();
               q.pop();
               for(int i=0;i<node->neighbers.size();i++){
                   degree[node->neighbers[i]->val]--;
                   if(degree[node->neighbers[i]->val]==0 ){
                       q.push(node->neighbers[i]);
                   }
               }
           }
           for(int i=0;i<graph.size();i++){
               delete graph[i];
           }
           for(int i=0;i<degree.size();i++){
               if(degree[i]){
                   return false;
               }
           }
           return true;
    }
};

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值