解决面试题的思路

本文详细解析了与二叉树相关的算法问题,包括如何实现二叉树的镜像、判断对称性、顺时针打印矩阵以及在栈中实现min函数等。此外,还探讨了复杂链表的复制、二叉搜索树转化为双向链表、序列化二叉树和字符串排列等复杂问题的简化解决方案。
摘要由CSDN通过智能技术生成

欢迎访问我的博客首页


1. 画图让抽象问题形象化


1.1 二叉树的镜像


  题目:请实现一个函数,输入一颗二叉树,输出它的镜像。二叉树与它的镜像如下图所示:

二叉树的镜像

图 1 图 1 1

  分析:从图 1 不难发现,依次交换每个结点的左右子树就可以得到一颗树的镜像,所以遍历每一个结点时交换它的子树即可。但因为操作本身是交换,所以请注意中序遍历。以图 1 中的右图为例:遍历根节点 A 前处理左子树 B,遍历 A 结点时把左子树 B 作为右子树,然后再处理右子树,这样就处理了两次 B 而没有处理 A。综上所述可以实现先序、后序、层序等遍历方法。

// 代码1。
TreeNode* invertTree(TreeNode* root) {
	if (root == nullptr)
		return nullptr;
	TreeNode* temp;
	temp = invertTree(root->right);
	root->right = invertTree(root->left);
	root->left = temp;
	return root;
}
// 代码2。
TreeNode* invertTree(TreeNode* root) {
	if (root == nullptr)
		return nullptr;
	TreeNode *lchild, *rchild;
	lchild = invertTree(root->right);
	rchild = invertTree(root->left);
	root->left = lchild;
	root->right = rchild;
	return root;
}
// 代码3。
TreeNode* invertTree(TreeNode* root) {
	if (root == nullptr)
		return nullptr;
	invertTree(root->right);
	invertTree(root->left);
	swap(root->left, root->right);
	return root;
}

  代码:注意交换两个子树时需要用到第 3 个变量,这和交换两个普通变量一样。代码 1 中使用第 3 个变量 temp 实现交换,代码 2 使用了两个额外变量。代码 3 更适合形参是根节点指针的指针或引用类型的形参,这样就不需要返回值。代码 3 虽然有返回值,但函数 invertTree 调用自身的返回值都没有被使用,如第 26、27 行。

1.2 对称的二叉树


  题目:输入一颗二叉树,判断它是不是对称的二叉树。如果二叉树和它的镜像一样,它就是对称的二叉树。如图 2 的左图的二叉树是对称的,中图和右图是不对称的。

对称的二叉树
图 2 图 2 2

  分析:判断二叉树是否对称的基本操作是比较结点的值是否相等,而关键就是谁和谁比较。从图 2 的左图可以发现,判断的过程是这样的:从根节点开始比较左右子节点,然后比较左子节点的左节点和右子节点的右节点、左子节点的右节点和右子节点的左节点…直到出现空结点或出现不相等。

bool equal(TreeNode* a, TreeNode* b) {
	if (a == nullptr && b == nullptr)
		return true;
	if (a == nullptr || b == nullptr)
		return false;
	if (a->val != b->val)
		return false;
	return equal(a->left, b->right) && equal(a->right, b->left);
}

bool isSymmetric(TreeNode* root) {
	if (root == nullptr)
		return false;
	return equal(root->left, root->right);
}

  代码:切记第 6 行不能放在前面,因为通过指针使用成员引用变量符时要确保指针非空。

1.3 顺时针打印矩阵


  题目:输入一个矩阵,按从外到内,以顺时针打印矩阵。例如图 3 的矩阵打印的顺序为 {1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10}。

顺时针打印
图 3 图 3 3

  分析:从图中我们可以看出,遍历一周分别向 4 个方向移动,比如打印 {1,2,3,4} 是向右移动,打印 {4,8,12,16} 是向下移动…我们只需要控制遍历时的移动范围就行了,而移动范围又是递减的。

2. 举例让抽象问题具体化


2.1 包含 min 函数的栈


  题目:定义栈,实现 3 个成员函数 min、push、pop,它们的时间复杂度都是 O(1),其中 min 函数返回栈内最小的元素。
  分析:定义一个辅助栈,每次向主栈添加元素后,向辅助栈添加一个主栈中最小的元素。调用 min 函数时返回辅助栈的栈顶元素。

2.2 栈的压入、弹出序列


  题目:输入栈的压入序列,判断另一个序列是否有可能是出栈序列。其中入栈的所有数字各不相等。
  分析:实现进栈出栈操作,验证能否按所给序列将全部元素出栈。
  我最开始想的方法是找规律:如果进栈顺序是 ABC,且 C 先于 AB 出栈,则出栈序列中 B 必在 A 之前。这种规律很容易理解,但实现算法比较繁琐。

bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
	stack<int> st;
	int index = 0;
	for (int i = 0; i < pushed.size(); i++) {
		st.push(pushed[i]);
		while (st.empty() != true && st.top() == popped[index]) {
			st.pop();
			index++;
		}
	}
	if (index != pushed.size())
		return false;
	return true;
}

  代码:对空栈可以执行 pop 操作,不能执行 top 操作,否则会出现异常。

2.3 从上到下打印二叉树


1. 不分行打印

  二叉树的层序遍历就是广度优先遍历,可以用队列实现。树是图的一种退化形式,所以广度优先遍历有向图也可以使用二叉树的层序遍历算法。

2. 分行打印

  题目:二叉树的层序遍历很容易把所有结点打印在一行。请实现从上到下打印二叉树,且每层占一行。
  分析:从二叉树的层序遍历可以发现:上一层第一个结点出栈时下一层的结点开始进栈,上一层最后一个结点出栈后下一层的结点全部进栈。据此,我们可以根据上一层的结点数得出下一层的结点数,而二叉树第一层结点数是 1,所以每一层的结点数都可以得出。

void layer_traverse(binaryTreeNode* tree) {
	if (tree == nullptr)
		return;
	queue<binaryTreeNode*> qu;
	qu.push(tree);
	int this_layer = 1, next_layer = 0;
	while (qu.empty() != true) {
		tree = qu.front();
		qu.pop();
		cout << tree->data << " ";
		this_layer--;
		if (tree->lchild != nullptr) {
			qu.push(tree->lchild);
			next_layer++;
		}
		if (tree->rchild != nullptr) {
			qu.push(tree->rchild);
			next_layer++;
		}
		if (this_layer == 0) {
			cout << endl;
			this_layer = next_layer;
			next_layer = 0;
		}
	}
}

3. 之字形分行打印

  题目:之字形打印二叉树:第 1 行打印二叉树第 1 层从左到右的结点,第 2 行打印二叉树第 2 层从右向左的结点,以此类推打印所有层。
  分析:上面用两种方法每行打印二叉树的一层。在此基础上,使用一个计数器记录遍历到哪一层。如果遍历到偶数层,不打印结点而是把结点入栈,遍历完该层后打印出栈结点。

2.4 二叉搜索树的后序遍历序列


  题目:输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。假设输入的数组的任意两个数字都互不相同。

后序遍历
图 4 图 4 4

  分析:对于二叉搜索树的每个结点,它的左子树上每个结点的值都比它小,右子树上每个结点的值都比它大。而后序遍历序列中,根结点在最后;如果有左子树,左子树序列在前部分;如果有右子树,右子树序列在左子树序列与根结点之间。所以二叉排序树的后序遍历序列中,如果存在比根结点小的部分,这部分都在左侧;如果存在比根节点大的部分,这部分都在中间。

bool varify_core(vector<int>& post, int start, int end) {
	// 1.只有根结点。
	if (start == end)
		return true;
	// 2.有右子树,可能有左子树。
	if (post[end - 1] > post[end]) {
		// 右子树是post[right]到post[end-1]。
		int right = end - 1;
		while (right - 1 >= start && post[right - 1] > post[end])
			right--;
		// 验证右子树。
		bool check_right = varify_core(post, right, end - 1);
		// 验证左子树的元素都大于根结点。
		for (int i = right - 1; i >= start; i--)
			if (post[i] > post[end])
				return false;
		// 验证左子树。
		bool check_left = true;
		if (right >= start)
			check_left = varify_core(post, start, right - 1);
		// 返回验证左右子树的结果。
		return check_left && check_right;
	}
	// 3.没有右子树,有左子树。
	else {
		// 验证左子树的元素都大于根结点。
		for (int i = end - 2; i >= start; i--)
			if (post[i] > post[end])
				return false;
		// 验证左子树。
		return varify_core(post, start, end - 1);
	}
}

bool verifyPostorder(vector<int>& postorder) {
	if (postorder.size() <= 0)
		return false;
	return varify_core(postorder, 0, postorder.size() - 1);
}

  代码:这个代码在牛客能通过。在力扣上第一个测试 [1,2,5,10,6,9,4,3] 就通不过,报越界错误,本地可以正常运行。

2.5 二叉树中和为某一值的路径


void get_path(TreeNode* root, int target, vector<vector<int>>& paths, vector<int>& path = vector<int>{}, int sum = 0) {
	if (root == nullptr)
		return;	
	path.push_back(root->val);
	sum += root->val;
	if (root->left == nullptr && root->right == nullptr && sum == target)
		paths.push_back(path);
	get_path(root->left, target, paths, path, sum);
	get_path(root->right, target, paths, path, sum);
	sum -= path.back();
	path.pop_back();
}

vector<vector<int>> pathSum(TreeNode* root, int target) {
	if (root == nullptr)
		return {};
	vector<vector<int>> paths;
	get_path(root, target, paths);
	sort(paths.begin(), paths.end());
	return paths;
}

  函数 get_path 的算法来源于二叉树的(先序、中序、后序)递归遍历算法。还可以根据二叉树非递归的后序遍历算法查找路径,这里不再说明。

3. 分解让复杂问题简单化


3.1 复杂链表的复制


  题目:复杂链表除了包含一个值域和一个指向下一个结点的指针域,还包含一个随机指针,这个随机指针可能指向任一个结点,也可能是空指针。请设计算法复制一个复杂链表。

1. 时间 O(n^2),空间 O(1)

  分析:第一步先复制得到新链表,新链表每个结点的 random 指针暂时为空。第二步为新链表每个结点的 random 指针赋值,赋值的根据是:如果旧链表第 i 个结点的 random 指针指向旧链表的第 j 个节点,则新链表第 i 个结点的 random 指针指向新链表的第 j 个节点。

void copy_normad_list(Node* newHead, Node* head) {
	while (head->next != nullptr) {
		newHead->next = new Node(head->next->val);
		newHead = newHead->next;
		head = head->next;
	}
}

Node* copyRandomList(Node* head) {
	if (head == nullptr)
		return nullptr;
	// 1.复制得到新链表newHead,random指针全为空。
	Node* newHead = new Node(head->val);
	copy_normad_list(newHead, head);
	// 2.给新链表的random指针赋值。
	Node *old_i = head, *new_i = newHead;
	Node *old_j = nullptr, *new_j = nullptr;
	while (old_i != nullptr) {
		old_j = head;
		new_j = newHead;
		while (old_j != old_i->random) {
			old_j = old_j->next;
			new_j = new_j->next;
		}
		new_i->random = new_j;
		old_i = old_i->next;
		new_i = new_i->next;
	}
	return newHead;
}

2. 时间 O(n),空间 O(n)

  分析:利用哈希表。random 指针和其它链表中的指针一样,存放的是结点的地址。把旧链表每个结点的地址与新链表每个结点的地址关联起来,知道旧链表第 i 个结点的 random 指针的值为 addres_old_j,通过关联信息找到 addres_old_j 对应的地址 addres_new_j 就是新链表第 i 个结点的 random 指针的值。下面使用哈希表 unordered_map 存储这些关联信息:

哈希表
图 5 图 5 5

Node* copyRandomList(Node* head) {
	if (head == nullptr)
		return nullptr;
	Node* newHead = new Node(head->val);
	Node *p1 = head, *p2 = newHead;
	unordered_map<Node*, Node*> umap;
	umap.insert(make_pair(p1, p2));
	while (p1->next != nullptr) {
		p2->next = new Node(p1->next->val);
		p1 = p1->next;
		p2 = p2->next;
		umap.insert(make_pair(p1, p2));
	}
	p1 = head;
	p2 = newHead;
	while (p1 != nullptr) {
		p2->random = umap[p1->random];
		p1 = p1->next;
		p2 = p2->next;
	}
	return newHead;
}

3. 时间 O(n),空间 O(1)

  分析:为了快速、不占额外内存地找到 random 指针的值,我们像下图 6 那样复制链表:把旧链表每个结点复制后放在它后面。比如复制结点 A 得到结点 A ′ A' A,结点 A 指向结点 C,那么复制后的结点 A ′ A' A 也指向结点 C。结点 A ′ A' A 应该指向新结点 C ′ C' C,而结点 C ′ C' C 就在结点 C 后。

复制链表
图 6 图 6 6

  所以该算法分为三部:第一步像 图 6 那样复制结点。第 2 步修改序号为偶数的结点 node 的 random 指针的值:node->random = node->random->next。第 3 步把复制的链表拆分出来。

void copy_node(Node* head) {
	while (head != nullptr) {
		Node* node = new Node(head->val);
		node->random = head->random;
		node->next = head->next;
		head->next = node;
		head = head->next->next;
	}
}

void assign_random(Node* head) {
	while (head != nullptr) {
		head = head->next;
		if (head->random != nullptr)
			head->random = head->random->next;
		head = head->next;
	}
}

Node* split_list(Node* head) {
	Node* newHead = head->next;
	Node* p = newHead;
	while (true) {
		head->next = p->next;
		head = p->next;
		if (head == nullptr)
			break;
		p->next = head->next;
		p = head->next;
	}
	return newHead;
}

Node* copyRandomList(Node* head) {
	if (head == nullptr)
		return nullptr;
	copy_node(head);
	assign_random(head);
	return split_list(head);
}

3.2 二叉搜索树与双向链表


  题目:输入一颗二叉搜索树,将它转换成一个排序的双向链表。要求:不能创建结点,只能调整树结点的指针。

二叉搜索树与双向链表
图 7 图 7 7

  为了在力扣上提交代码,按照力扣的要求让双向链表首尾相连,如第 19、23 行。

void inOrder(Node* root, vector<Node*>& res) {
	if (root == nullptr)
		return;
	inOrder(root->left, res);
	res.push_back(root);
	inOrder(root->right, res);
}

Node* treeToDoublyList(Node* root) {
	if (root == nullptr)
		return nullptr;
	vector<Node*> inOrderRes;
	// 1.得到中序遍历序列。
	inOrder(root, inOrderRes);
	int size = inOrderRes.size();
	// 2.右指针指向递增的方向。
	for (int i = 0; i < size - 1; i++)
		inOrderRes[i]->right = inOrderRes[i + 1];
	inOrderRes[size - 1]->right = inOrderRes[0];
	// 3.左指针指向递减的方向。
	for (int i = size - 1; i > 0; i--)
		inOrderRes[i]->left = inOrderRes[i - 1];
	inOrderRes[0]->left = inOrderRes[size - 1];
	return inOrderRes[0];
}

  改进:没有必要使用 vector 存放指针:第一步中序遍历时改变它们的左指针,第二步改变它们的右指针:

void inOrder(Node* root, Node*& min) {
	if (root == nullptr)
		return;
	inOrder(root->left, min);
	root->left = min;
	min = root;
	inOrder(root->right, min);
}

Node* treeToDoublyList(Node* root) {
	if (root == nullptr)
		return nullptr;
	// 1.中序遍历得到指向最大结点的指针。
	Node* max = nullptr;
	inOrder(root, max);
	// 2.右指针指向递增的方向。
	Node *p = max->left, *min = max;
	while (p != nullptr) {
		p->right = min;
		p = p->left;
		min = min->left;
	}
	// 3.让首尾相指。
	min->left = max;
	max->right = min;
	return min;
}

  使用这种方法只能让左指针指向递减的方向。因为只有中序遍历才能从二叉搜索树得到排序的序列,而我们在中序遍历时只能改变被访问结点的左指针。这是因为被访问结点的左子树已经被访问过了,而右子树还没访问,所以不能改变右指针。
  使用额外空间时,左右指针可以随意指向递减或递增的方向。

3.3 序列化二叉树


3.4 字符串的排列


  题目:在 n × n n \times n n×n 的国际象棋上摆放 n 个皇后,任意两个皇后不能在同一行、同一列、同一对角线上。请设计算法计算有多少摆放方法。
  分析:不同行则纵坐标各不相同,不同列则横坐标各不相同,因此每列有且只有一个皇后,每行也有且只有一个皇后。假设第 i 列的皇后在第 y[i] 行放着,则 y 是 0 到 n-1 的全排列中的一个排列。
  也就是说,我们先获取 0 到 n-1 的全排列,然后取出一个排列 y,那么 n 个皇后的坐标就是 (0, y[0]), (1, y[1]), …, (n-1, y[n-1])。显然这已经满足不同行且不同列,接下来只需判断任意两个皇后在不在对角线,即判断 abs(y[i] - y[j]) 是否等于 abs(i - j)。

int totalNQueens(int n) {
	string s;
	for (int i = 0; i < n; i++)
		s.push_back('0' + i);
	set<string> st = permutation(s);
	int res = 0;
	for (auto y : st) {
		bool flag = true;
		for (int i = 0; i < n; i++)			// 坐标:(i, y[i])
			for (int j = 0; j < n; j++) {	// 坐标:(j, y[j])
				if (i == j)
					continue;
				if (abs(y[j] - y[i]) == abs(j - i)) {
					flag = false;
					j = n;
					i = n;
				}
			}
		if (flag == true)
			res++;
	}
	return res;
}

4. 参考


  1. 序列化二叉树
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
4S店客户管理小程序-毕业设计,基于微信小程序+SSM+MySql开发,源码+数据库+论文答辩+毕业论文+视频演示 社会的发展和科学技术的进步,互联网技术越来越受欢迎。手机也逐渐受到广大人民群众的喜爱,也逐渐进入了每个用户的使用。手机具有便利性,速度快,效率高,成本低等优点。 因此,构建符合自己要求的操作系统是非常有意义的。 本文从管理员、用户的功能要求出发,4S店客户管理系统中的功能模块主要是实现管理员服务端;首页、个人中心、用户管理、门店管理、车展管理、汽车品牌管理、新闻头条管理、预约试驾管理、我的收藏管理、系统管理,用户客户端:首页、车展、新闻头条、我的。门店客户端:首页、车展、新闻头条、我的经过认真细致的研究,精心准备和规划,最后测试成功,系统可以正常使用。分析功能调整与4S店客户管理系统实现的实际需求相结合,讨论了微信开发者技术与后台结合java语言和MySQL数据库开发4S店客户管理系统的使用。 关键字:4S店客户管理系统小程序 微信开发者 Java技术 MySQL数据库 软件的功能: 1、开发实现4S店客户管理系统的整个系统程序; 2、管理员服务端;首页、个人中心、用户管理、门店管理、车展管理、汽车品牌管理、新闻头条管理、预约试驾管理、我的收藏管理、系统管理等。 3、用户客户端:首页、车展、新闻头条、我的 4、门店客户端:首页、车展、新闻头条、我的等相应操作; 5、基础数据管理:实现系统基本信息的添加、修改及删除等操作,并且根据需求进行交流信息的查看及回复相应操作。
现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本微信小程序医院挂号预约系统就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数据信息,使用这种软件工具可以帮助管理人员提高事务处理效率,达到事半功倍的效果。此微信小程序医院挂号预约系统利用当下成熟完善的SSM框架,使用跨平台的可开发大型商业网站的Java语言,以及最受欢迎的RDBMS应用软件之一的MySQL数据库进行程序开发。微信小程序医院挂号预约系统有管理员,用户两个角色。管理员功能有个人中心,用户管理,医生信息管理,医院信息管理,科室信息管理,预约信息管理,预约取消管理,留言板,系统管理。微信小程序用户可以注册登录,查看医院信息,查看医生信息,查看公告资讯,在科室信息里面进行预约,也可以取消预约。微信小程序医院挂号预约系统的开发根据操作人员需要设计的界面简洁美观,在功能模块布局上跟同类型网站保持一致,程序在实现基本要求功能时,也为数据信息面临的安全问题提供了一些实用的解决方案。可以说该程序在帮助管理者高效率地处理工作事务的同时,也实现了数据信息的整体化,规范化与自动化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值