剑指offer_13(最后6题)来源:牛客网
1. 序列化二叉树
题目:
请实现两个函数,分别用来序列化和反序列化二叉树
二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),以 ! 表示一个结点值的结束(value!)。
二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。
参考代码:
/*
算法思想:根据前序遍历规则完成序列化与反序列化。所谓序列化指的是遍历二叉树为字符串;所谓反序列化指的是依据字符串重新构造成二叉树。
依据前序遍历序列来序列化二叉树,因为前序遍历序列是从根结点开始的。当在遍历二叉树时碰到Null指针时,这些Null指针被序列化为一个特殊的字符“#”。
另外,结点之间的数值用逗号隔开。
*/
public class Solution {
int index = -1; //计数变量
String Serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
if(root == null){
sb.append("#,");
return sb.toString();
}
sb.append(root.val + ",");
sb.append(Serialize(root.left));
sb.append(Serialize(root.right));
return sb.toString();
}
TreeNode Deserialize(String str) {
index++;
//int len = str.length();
//if(index >= len){
// return null;
// }
String[] strr = str.split(",");
TreeNode node = null;
if(!strr[index].equals("#")){
node = new TreeNode(Integer.valueOf(strr[index]));
node.left = Deserialize(str);
node.right = Deserialize(str);
}
return node;
}
}
//其它
import java.util.*;
//采用层序遍历,不需要将转化为完全二叉树的简单方法
public class Solution {
String Serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
Queue<TreeNode> queue = new LinkedList<TreeNode>();
if(root != null)
queue.add(root);
while(!queue.isEmpty()){
TreeNode node = queue.poll();
if(node != null){
queue.offer(node.left);
queue.offer(node.right);
sb.append(node.val + ",");
}else{
sb.append("#" + ",");
}
}
if(sb.length() != 0)
sb.deleteCharAt(sb.length()-1);
return sb.toString();
}
TreeNode Deserialize(String str) {
TreeNode head = null;
if(str == null || str.length() == 0)
return head;
String[] nodes = str.split(",");
TreeNode[] treeNodes = new TreeNode[nodes.length];
for(int i=0; i<nodes.length; i++){
if(!nodes[i].equals("#"))
treeNodes[i] = new TreeNode(Integer.valueOf(nodes[i]));
}
for(int i=0, j=1; j<treeNodes.length; i++){
if(treeNodes[i] != null){
treeNodes[i].left = treeNodes[j++];
treeNodes[i].right = treeNodes[j++];
}
}
return treeNodes[0];
}
}
//前序遍历
public class Solution {
String Serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
getSerializeString(root, sb);
if(sb.length() != 0)
sb.deleteCharAt(sb.length()-1);
return sb.toString();
}
getSerializeString(TreeNode root, StringBuilder sb){
if(root == null)
sb.append("#,");
else{
sb.append(root.val + ",");
getSerializeString(root.left, sb);
getSerializeString(root.right, sb);
}
}
TreeNode Deserialize(String str) {
if(str == null || str.length() == 0 || str.length() ==1)
return null;
String[] nodes = str.split(",");
TreeNode[] treeNodes = new TreeNode[nodes.length];
for(int i=0; i<nodes.length; i++){
if(!nodes[i].equals("#"))
treeNodes[i] = new TreeNode(Integer.valueOf(nodes[i]));
}
Stack<TreeNode> stack = new Stack<>();
stack.push(treeNodes[0]);
int i = 1;
while(treeNodes[i] != null){
stack.peek().left = treeNodes[i];
stack.push(treeNodes[i++]);
}
while(!stack.isEmpty()){
stack.pop().right = treeNodes[++i];
if(treeNodes[i] != null){
stack.push(treeNodes[i++]);
while(treeNodes[i] != null){
stack.peek().left = treeNodes[i];
stack.push(treeNodes[i++]);
}
}
}
return treeNodes[0];
}
}
2. 二叉搜索树的第k个结点
题目:
给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。
参考代码:
//我的做法
import java.util.*;
public class Solution {
ArrayList<TreeNode> list = new ArrayList<>();
TreeNode KthNode(TreeNode pRoot, int k)
{
if(k<1)
return null;
MidSearch(pRoot);
if(k>list.size())
return null;
return list.get(k-1);
}
void MidSearch(TreeNode pRoot){
if(pRoot != null){
MidSearch(pRoot.left);
list.add(pRoot);
MidSearch(pRoot.right);
}
}
}
//思路:二叉搜索树按照中序遍历的顺序打印出来正好就是排序好的顺序。
// 所以,按照中序遍历顺序找到第k个结点就是结果。
public class Solution {
int index = 0; //计数器
TreeNode KthNode(TreeNode root, int k)
{
if(root != null){ //中序遍历寻找第k个
TreeNode node = KthNode(root.left,k);
if(node != null)
return node;
index ++;
if(index == k)
return root;
node = KthNode(root.right,k);
if(node != null)
return node;
}
return null;
}
}
//非递归中序遍历
TreeNode KthNode(TreeNode root, int k){
if(root==null||k==0)
return null;
Stack<TreeNode> stack = new Stack<TreeNode>();
int count = 0;
TreeNode node = root;
do{
if(node!=null){
stack.push(node);
node = node.left;
}else{
node = stack.pop();
count++;
if(count==k)
return node;
node = node.right;
}
}while(node!=null||!stack.isEmpty());
return null;
}
3. 数据流中的中位数
题目:
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
参考代码:
【java】这里讨论两种方法:
一:代码复杂:减少不必要插入,提高效率
二:代码大大简化:可能有不必要插入,效率有所降低
==============思路解析=================================
思考:如何得到一个数据流中的中位数?
如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。
如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
一:代码复杂:
* 分析:对于海量数据和流的数据,用最大堆和最小堆来管理
* 我们希望 数据分为[小]|[大]两个部分,细化一点
[最大堆 | 左边最大 leftMax]
右边最小rightMin | 最小堆]
* 定义一个规则:保证左边和右边个数相差不大于1,且左边小于右边
* 1.数据是偶数的时候 insert的数据进入 [右边,最小堆]中
* 1.1当插入的数字cur > leftMax时,直接插入到[右边,最小堆]中
* 1.2当插入的数字cur < leftMax时,为了保证左边小于右边,
* 先把cur插入[最大堆|左边最大leftMax]中,
* 然后把leftMax放入[右边最小rightMin|最小堆]中
* 就可以保证: 左边 < 右边
* 2.数据是奇数的时候 insert的数据进入 [左边,最大堆]中
* 2.1当插入的数字cur < rightMin时,直接插入到[左边,最小堆]中
* 2.2当插入的数字cur > rightMin时,为了保证左边小于右边,
* 先把cur插入[右边最小rightMin|最小堆]中,
* 然后把rightMin放入[最大堆|左边最大leftMax]中
* 就可以保证: 左边 < 右边
* 最后:
* 如果是偶数:中位数mid= (leftMax+right)/2
* 如果是奇数:中位数mid= leftMax 因为先插入到左边,再插入到右边,为奇数时,中位数就是mid
//降序就是最大堆,lambda表达式了解下
private static PriorityQueue<Integer> leftHeap = new PriorityQueue<>((x, y) -> y - x);
//升序就是最小堆
private static PriorityQueue<Integer> rightHeap = new PriorityQueue<>();
//当前是奇数还是偶数
private static boolean isOdd = true;
public static void Insert(Integer num) {
if(isOdd) {//数据是奇数的时候 insert的数据进入 [左边,最大堆]中
if(leftHeap.isEmpty()) {
leftHeap.add(num);
}
else {//这个时候rightHeap一定不是null,就不讨论了。考虑鲁棒性可以讨论
int rightMin = rightHeap.peek();
if(num <= rightMin) {//直接插入到[左边,最小堆]中
leftHeap.add(num);
}else {
rightHeap.add(num);//先把cur插入[右边最小rightMin|最小堆]中
leftHeap.add(rightHeap.poll());//然后把rightMin放入[最大堆|左边最大leftMax]中
}
}
}else {//数据是偶数的时候 insert的数据进入 [右边,最小堆]中
//这个时候leftHeap一定不是null,就不讨论了。考虑鲁棒性可以讨论
int leftMax = leftHeap.peek();
if(num >= leftMax) {//直接插入到[右边,最小堆]中
rightHeap.add(num);
}else {
leftHeap.add(num);//先把cur插入[右边最小rightMin|最小堆]中,
rightHeap.add(leftHeap.poll());//然后把rightMin放入[最大堆|左边最大leftMax]中
}
}
isOdd = !isOdd;//改变奇偶性
}
public static Double GetMedian() {
if(leftHeap.isEmpty()) return 0.0;
if(!isOdd)//这里插入num改变了奇偶性,这里是奇数的意思
return leftHeap.peek() / 1.0;
else
return (leftHeap.peek() + rightHeap.peek()) / 2.0;
}
二.简化代码,取消了判断过程,直接插入到对面的堆中,然后再转移到自己的堆中
* 但是!!!时间复杂度提高,每次都插入时间复杂度O(log n)这是很可怕的
* 定义一个规则:不要判断了
* 1.数据是偶数的时候 insert的数据进入 [右边,最小堆]中
* 先把cur插入[最大堆|左边最大leftMax]中,
* 然后把leftMax放入[右边最小rightMin|最小堆]中
* 就可以保证: 左边 < 右边
* 2.数据是奇数的时候 insert的数据进入 [左边,最大堆]中
* 先把cur插入[右边最小rightMin|最小堆]中,
* 然后把rightMin放入[最大堆|左边最大leftMax]中
* 就可以保证: 左边 < 右边
* 最后:
* 如果是偶数:中位数mid= (leftMax+right)/2
* 如果是奇数:中位数mid= leftMax
看看是不是简化了很多,哈哈!
链接:https://www.nowcoder.com/questionTerminal/9be0172896bd43948f8a32fb954e1be1
来源:牛客网
//降序就是最大堆,lambda表达式了解下
private static PriorityQueue<Integer> leftHeap = new PriorityQueue<>((x, y) -> y - x);
private static PriorityQueue<Integer> rightHeap = new PriorityQueue<>();
private static boolean isOdd = true;
public static void Insert(Integer num) {
if(isOdd) {
rightHeap.add(num);
leftHeap.add(rightHeap.poll());
}else {
leftHeap.add(num);
rightHeap.add(leftHeap.poll());
}
isOdd = !isOdd;
}
public static Double GetMedian() {
if(leftHeap.isEmpty()) return 0.0;
if(!isOdd)
return leftHeap.peek() / 1.0;
else
return (leftHeap.peek() + rightHeap.peek()) / 2.0;
}
4. 滑动窗口的最大值
题目:
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
参考代码:
//我的做法
import java.util.*;
public class Solution {
ArrayList<Integer> res = new ArrayList<>();
public ArrayList<Integer> maxInWindows(int [] num, int size)
{
if(num==null || size<1)
return res;
for(int i=0;i<=num.length-size;i++){
int max=num[i];
for(int j=0;j<size;j++){
if(num[i+j]>max)
max = num[i+j];
}
res.add(max);
}
return res;
}
}
//其它
import java.util.*;
/**
用一个双端队列,队列第一个位置保存当前窗口的最大值,当窗口滑动一次
1.判断当前最大值是否过期
2.新增加的值从队尾开始比较,把所有比他小的值丢掉
*/
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size)
{
ArrayList<Integer> res = new ArrayList<>();
if(size == 0) return res;
int begin;
ArrayDeque<Integer> q = new ArrayDeque<>();
for(int i = 0; i < num.length; i++){
begin = i - size + 1;
if(q.isEmpty())
q.add(i);
else if(begin > q.peekFirst())
q.pollFirst();
while((!q.isEmpty()) && num[q.peekLast()] <= num[i])
q.pollLast();
q.add(i);
if(begin >= 0)
res.add(num[q.peekFirst()]);
}
return res;
}
}
//another
import java.util.*;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size) {
if (num == null || num.length == 0 || size <= 0 || num.length < size) {
return new ArrayList<Integer>();
}
ArrayList<Integer> result = new ArrayList<>();
//双端队列,用来记录每个窗口的最大值下标
LinkedList<Integer> qmax = new LinkedList<>();
for (int i = 0; i < num.length; i++) {
while (!qmax.isEmpty() && num[qmax.peekLast()] < num[i]) {
qmax.pollLast();
}
qmax.addLast(i);
//判断队首元素是否过期
if (qmax.peekFirst() == i - size) {
qmax.pollFirst();
}
//向result列表中加入元素
if (i >= size - 1) {
result.add(num[qmax.peekFirst()]);
}
}
return result;
}
}
//其它,deque s中存储的是num的下标
class Solution {
public:
vector<int> maxInWindows(const vector<int>& num, unsigned int size)
{
vector<int> res;
deque<int> s;
for(unsigned int i=0;i<num.size();++i){
while(s.size() && num[s.back()]<=num[i])//从后面依次弹出队列中比当前num值小的元素,同时也能保证队列首元素为当前窗口最大值下标
s.pop_back();
while(s.size() && i-s.front()+1>size)//当当前窗口移出队首元素所在的位置,即队首元素坐标对应的num不在窗口中,需要弹出
s.pop_front();
s.push_back(i);//把每次滑动的num下标加入队列
if(size&&i+1>=size)//当滑动窗口首地址i大于等于size时才开始写入窗口最大值
res.push_back(num[s.front()]);
}
return res;
}
};
5. 矩阵中的路径
题目:
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e 矩阵中包含一条字符串"bccced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
参考代码:
public class Solution {
public boolean hasPath(char[] matrix, int rows, int cols, char[] str)
{
//标志位,初始化为false
boolean[] flag = new boolean[matrix.length];
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
//循环遍历二维数组,找到起点等于str第一个元素的值,再递归判断四周是否有符合条件的----回溯法
if(judge(matrix,i,j,rows,cols,flag,str,0)){
return true;
}
}
}
return false;
}
//judge(初始矩阵,索引行坐标i,索引纵坐标j,矩阵行数,矩阵列数,待判断的字符串,字符串索引初始为0即先判断字符串的第一位)
private boolean judge(char[] matrix,int i,int j,int rows,int cols,boolean[] flag,char[] str,int k){
//先根据i和j计算匹配的第一个元素转为一维数组的位置
int index = i*cols+j;
//递归终止条件
if(i<0 || j<0 || i>=rows || j>=cols || matrix[index] != str[k] || flag[index] == true)
return false;
//若k已经到达str末尾了,说明之前的都已经匹配成功了,直接返回true即可
if(k == str.length-1)
return true;
//要走的第一个位置置为true,表示已经走过了
flag[index] = true;
//回溯,递归寻找,每次找到了就给k加一,找不到,还原
if(judge(matrix,i-1,j,rows,cols,flag,str,k+1) ||
judge(matrix,i+1,j,rows,cols,flag,str,k+1) ||
judge(matrix,i,j-1,rows,cols,flag,str,k+1) ||
judge(matrix,i,j+1,rows,cols,flag,str,k+1) )
{
return true;
}
//走到这,说明这一条路不通,还原,再试其他的路径
flag[index] = false;
return false;
}
}
6. 机器人的运动范围
题目:
地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
参考代码:
核心思路:
1.从(0,0)开始走,每成功走一步标记当前位置为true,然后从当前位置往四个方向探索,
返回1 + 4 个方向的探索值之和。
2.探索时,判断当前节点是否可达的标准为:
1)当前节点在矩阵内;
2)当前节点未被访问过;
3)当前节点满足limit限制。
public class Solution {
public int movingCount(int threshold, int rows, int cols) {
boolean[][] visited = new boolean[rows][cols];
return countingSteps(threshold,rows,cols,0,0,visited);
}
public int countingSteps(int limit,int rows,int cols,int r,int c,boolean[][] visited){
if (r < 0 || r >= rows || c < 0 || c >= cols
|| visited[r][c] || bitSum(r) + bitSum(c) > limit) return 0;
visited[r][c] = true;
return countingSteps(limit,rows,cols,r - 1,c,visited)
+ countingSteps(limit,rows,cols,r,c - 1,visited)
+ countingSteps(limit,rows,cols,r + 1,c,visited)
+ countingSteps(limit,rows,cols,r,c + 1,visited)
+ 1;
}
public int bitSum(int t){
int count = 0;
while (t != 0){
count += t % 10;
t /= 10;
}
return count;
}
}
//python
//思路:将地图全部置1,遍历能够到达的点,将遍历的点置0并令计数+1.这个思路在找前后左右相连的点很有用,比如leetcode中的海岛个数问题/最大海岛问题都可以用这种方法来求解。
class Solution:
def __init__(self):
self.count = 0
def movingCount(self, threshold, rows, cols):
# write code here
arr = [[1 for i in range(cols)] for j in range(rows)]
self.findway(arr, 0, 0, threshold)
return self.count
def findway(self, arr, i, j, k):
if i < 0 or j < 0 or i >= len(arr) or j >= len(arr[0]):
return
tmpi = list(map(int, list(str(i))))
tmpj = list(map(int, list(str(j))))
if sum(tmpi) + sum(tmpj) > k or arr[i][j] != 1:
return
arr[i][j] = 0
self.count += 1
self.findway(arr, i + 1, j, k)
self.findway(arr, i - 1, j, k)
self.findway(arr, i, j + 1, k)
self.findway(arr, i, j - 1, k)