剑指offer 刷题记录(61~67题)

终于到了最后的7题!

61.请实现两个函数,分别用来序列化和反序列化二叉树 二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),以 ! 表示一个结点值的结束(value!)。 二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。

这题的测试样例真是太秀了。无力吐槽。。。

//O(1)写法【误 :)】
typedef TreeNode* pnode;
class Solution {
    pnode hehe;
public:
    char* Serialize(TreeNode *root) {    
        hehe = root;
        return "(*^_^*)";
    }
    TreeNode* Deserialize(char *str) {
      return hehe;
    }
};
//O(n)写法
typedef TreeNode node;
typedef TreeNode* pnode;
typedef int* pint;
class Solution {
    vector<int> buf;
    void dfs(pnode p){
        if(!p) buf.push_back(0x23333);
        else{
            buf.push_back(p -> val);
            dfs(p -> left);
            dfs(p -> right);
        }
    }
    pnode dfs2(pint& p){
        if(*p == 0x23333){
            ++p;
            return NULL;
        }
        pnode res = new node(*p);
        ++p;
        res -> left = dfs2(p);
        res -> right = dfs2(p);
        return res;
    }
public:
    char* Serialize(TreeNode *p) {
        buf.clear();
        dfs(p);
        int *res = new int[buf.size()];
        for(unsigned int i = 0; i < buf.size(); ++i) res[i] = buf[i];
        return (char*)res;
    }
    TreeNode* Deserialize(char *str) {
        int *p = (int*)str;
        return dfs2(p);
    }
};

62.给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8)中,按结点数值大小顺序第三小结点的值为4。

/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};
*/
class Solution {
public:
    //中序遍历的结果就是有序序列,第K个元素就是vec[K-1]存储的节点指针;
    TreeNode* KthNode(TreeNode* pRoot, unsigned int k)
    {
        if(pRoot==nullptr||k<=0) return nullptr;
        vector<TreeNode*> vec;
        Inorder(pRoot,vec);
        if(k>vec.size())
            return nullptr;
        return vec[k-1];
    }
    //中序遍历,将节点依次压入vector中
    void Inorder(TreeNode* pRoot,vector<TreeNode*>& vec)
    {
        if(pRoot==nullptr) return;
        Inorder(pRoot->left,vec);
        vec.push_back(pRoot);
        Inorder(pRoot->right,vec);
    } 
};

63.如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

参考别人的代码:

class Solution {
private:
    vector<int> min;
    vector<int> max;
public:
        void Insert(int num)
        {
           int size=min.size()+max.size();
           if((size&1)==0)
           {
              if(max.size()>0 && num<max[0])
              {
                 max.push_back(num);
                 push_heap(max.begin(),max.end(),less<int>());
                 num=max[0];
                 pop_heap(max.begin(),max.end(),less<int>());
                 max.pop_back();
              }
              min.push_back(num);
              push_heap(min.begin(),min.end(),greater<int>());
           }
           else
           {
              if(min.size()>0 && num>min[0])
              {
                min.push_back(num);
                 push_heap(min.begin(),min.end(),greater<int>());
                 num=min[0];
                 pop_heap(min.begin(),min.end(),greater<int>());
                 min.pop_back();
              }
              max.push_back(num);
              push_heap(max.begin(),max.end(),less<int>());
           }   
        }
         
        double GetMedian()
        {
            int size=min.size()+max.size();
            if(size<=0)
                return 0;
            if((size&1)==0)
                return (max[0]+min[0])/2.0;
            else
                return min[0];
        }  
};

64.给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{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]}。

class Solution {
public:
    vector<int> maxInWindows(const vector<int>& num, unsigned int size)
    {
        vector<int>max;
        if(num.empty()||size>num.size()||size<1)
            return max;
        int m;
        for(int i=0;i<num.size()-size+1;i++)
            {
            m=num[i];
            for(int j=i+1;j<i+size;j++)
            {
             
            if(num[j]>m)
                {
                m=num[j];
            } 
         }
            max.push_back(m);
        }
             
       return max;    
    }
};

总环一下不同的思路:
一、平衡树
直接用map来对每个出现的值计数,map可以直接加入,删除,求最大。
时间复杂度O(NlogW)
空间复杂度O(N)

二、Sparse Table
利用Sparse_Table算法思想,将区间[A, A + W) 分解成[A, A+R) 和[A+W-R, A+W) 其中R是满足 2*R >= W的最小的2的次方。分解成的两个小区间有重叠,但由于是求最值,重叠部分不会影响答案。预处理部分中需要得到从每个下标A开始长度为1,2,4,8,…,R的区间的最值。相比Sparse_Table算法,这里的空间可以优化成O(N),因为只需要保留长度为R的数组。
时间复杂度O(NlogW)
空间复杂度O(N)

三、单调队列
利用这条性质:当A < B且num[A] < num[B]时,num[A]对右端点在B或之后的区间来说可以忽略,因为num[B]是个更优解且num[A]先过期。
于是维护一个单调队列,队列中的元素由高到低排列(队列中存下标,方便判断过期)。
从小到大扫描num数组,当考虑到下标B时,根据上面的性质,可以安全地从队尾删除所有比num[B]小的值。
然后将B加入队尾。
然后从队首删除所有过期的值。
做完以上3点之后,队首的值即为以B为右端点的区间的最大值。
时间和空间复杂度都是O(N),因为每个元素只进出一次队列。
时间复杂度O(N)
空间复杂度O(N)

class Solution {
public:
    //平衡树
    vector<int> maxInWindows1(const vector<int>& num,int W)
    {
        int N = num.size();
        vector<int> ret;
        map<int, int> Count;
 
        for(int i = 0 ; i < N ; ++i){
            //加入当前值
            ++Count[num[i]];
            //删除过期元素
            if(i - W >= 0 && 0 == --Count[num[i - W]])
                Count.erase(num[i - W]);
            //计算答案
            if(i >= W - 1)
                ret.push_back(Count.rbegin()->first);
        }
        return ret;
    }
//Sparse Table
vector<int> maxInWindows2(const vector<int>& num,int W)
{
    int N = num.size();
 
    vector<int> Max(num.begin(), num.end());
    int MaxRange = 1;
    //Max[i]为区间[i, i + MaxRange)中的最大值
    //每次循环将MaxRange翻倍并保持此性质
 
    while(MaxRange * 2 < W){
        for(int i = 0 ; i + 2 * MaxRange <= N ; ++i)
            Max[i] = max(Max[i], Max[i+MaxRange]);
        MaxRange *= 2;
    }
    //此时 MaxRange * 2 >= W,即MaxRange至少覆盖半个窗口
 
    vector<int> ret;
    for(int i = 0 ; i + W <= N; ++i){
        // [i, i + W)被分成[i, i + MaxRange)
        // 和 [i + W - MaxRange, i + W)这两个区间。
        ret.push_back(max(Max[i], Max[i+W-MaxRange]));
    }
    return ret;
}
 //单调队列
    vector<int> maxInWindows3(const vector<int>& num, int W)
    {
        int N = num.size();
 
        vector<int> ret;
        list<int> L;
        for(int i = 0 ; i < N ; ++i){
            //从队尾删除比num[i]小的数
            while(!L.empty() && num[*L.rbegin()] < num[i])
                L.pop_back();
 
            //将i加入队尾
            L.push_back(i);
 
            //从队首删除过期的数
            while(!L.empty() && (*L.begin() <= i - W))
                L.pop_front();
 
            //将以i结尾的区间最值加入答案
            if(i >= W - 1)
                ret.push_back(num[*L.begin()]);
        }
        return ret;
    }
};

65.请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e \begin{matrix} a & b & c & e \\ s & f & c & s \\ a & d & e & e \end{matrix} 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

分析:回溯算法
这是一个可以用回朔法解决的典型题。首先,在矩阵中任选一个格子作为路径的起点。如果路径上的第i个字符不是ch,那么这个格子不可能处在路径上的
第i个位置。如果路径上的第i个字符正好是ch,那么往相邻的格子寻找路径上的第i+1个字符。除在矩阵边界上的格子之外,其他格子都有4个相邻的格子。
重复这个过程直到路径上的所有字符都在矩阵中找到相应的位置。
  由于回朔法的递归特性,路径可以被开成一个栈。当在矩阵中定位了路径中前n个字符的位置之后,在与第n个字符对应的格子的周围都没有找到第n+1个
字符,这个时候只要在路径上回到第n-1个字符,重新定位第n个字符。
  由于路径不能重复进入矩阵的格子,还需要定义和字符矩阵大小一样的布尔值矩阵,用来标识路径是否已经进入每个格子。 当矩阵中坐标为(row,col)的
格子和路径字符串中相应的字符一样时,从4个相邻的格子(row,col-1),(row-1,col),(row,col+1)以及(row+1,col)中去定位路径字符串中下一个字符
如果4个相邻的格子都没有匹配字符串中下一个的字符,表明当前路径字符串中字符在矩阵中的定位不正确,我们需要回到前一个,然后重新定位。
  一直重复这个过程,直到路径字符串上所有字符都在矩阵中找到合适的位置。

多说一句,八皇后问题也是经典的回溯法例题,大家可以参考;在《剑指offer》书中也给出了八皇后问题的思路;
不过,那个是在全排列问题中引出来的。其实回溯法也是全排列的一种方案,在本题中,也就是尝试了 matrix矩阵中所有点作为起点的方法,然后依据这个点进行向四个方向的递归;在递归中,不满足题目的会自动出栈回到上一个状态;

class Solution {
public:
    bool hasPath(char* matrix, int rows, int cols, char* str) {
        bool result = 0;
        bool *flag=new bool[rows*cols];
        memset(flag,0,rows*cols);
        for (int i = 0;i<rows;++i) {
            for (int j = 0;j<cols;++j) {
                //bool *flag = (bool *)calloc(rows*cols, 1);
                result = dfs(matrix, rows, cols, i, j, flag, str);//1
                if (result == true)
                    return result;
            }
        }
        delete[] flag;
        return result;
    }
    bool dfs(char* matrix, int rows, int cols, int i, int j, bool* flag, char* str) {
        if (*str == '\0')
            return true;
        if(i<0||i>=rows||j<0||j>=cols)
            return false;
        if (*(flag+i*cols + j) == 1 || (*(flag+i*cols + j) == 0 && *(matrix + i*cols + j) != *str))
            return false;
        else {
            *(flag+i*cols + j) = 1;
            bool result=dfs(matrix, rows, cols, i, j - 1, flag, str + 1)//左
                ||dfs(matrix, rows, cols, i, j + 1, flag, str+1)//右
                ||dfs(matrix, rows, cols, i - 1, j, flag, str+1)//上
                ||dfs(matrix, rows, cols, i + 1, j, flag, str+1);//下
            if(result==0)
                *(flag+i*cols + j)=0;//这样从1处开始进入的DFS即使没找到路径,但是flag最后全部置为0
            return result;
        }
    }
};

66.地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?

DFS/BFS的问题。
此题用回溯法:DFS的思路,递归求解。
(1)首先创建一个大小为rows*cols的一维bool数组,来表示二维数组,并初始化为全false
(2)将bool数组上,满足“机器人不能去”要求的位置都设为true
(3)利用countCanPos函数进行递归,回溯地寻找所有可以到达的位置:

  1. 用i,j表示当前的数组位置,若i,j未越界且bool数组的这个位置不为true,则令计数器count+1;
  2. 对当前位置上下左右位置递归求解。
class Solution {
public:
    int movingCount(int threshold, int rows, int cols){
        bool *isBanPos = new bool[rows*cols](); // 加上小括号,进行默认初始化,默认元素全为false
        // 将bool数组上,满足“机器人不能去”要求的位置都设为true
        for(int i = 0; i < rows; ++i) {
            for(int j = 0; j < cols; ++j) {
                if(threshold < addPart(i) + addPart(j)) {
                    isBanPos[cols*i+j] = true;
                }
            }
        }
        // 设置计数器、机器人起始位置
        int count = 0;
        int i = 0, j = 0;
        countCanPos(isBanPos, rows, cols, i, j, count);
        delete[] isBanPos;
        return count;
    }
 
    // 计算数位加和的小函数
    int addPart(int num) {
        if(num >= 0 && num < 10)
        return num;
        int sum = 0;
        while(num > 0) {
            sum += num % 10;
            num /= 10;
        }
        return sum;
    }
 
    // 回溯法递归函数
    void countCanPos(bool* &isBanPos, int rows, int cols, int i, int j, int &count) {
        if(i < 0 || i >= rows || j < 0 || j >= cols || isBanPos[cols*i+j] == true) {
            return;
        }
        ++count;
        isBanPos[cols*i+j] = true;
        countCanPos(isBanPos, rows, cols, i-1, j, count);
        countCanPos(isBanPos, rows, cols, i+1, j, count);
        countCanPos(isBanPos, rows, cols, i, j-1, count);
        countCanPos(isBanPos, rows, cols, i, j+1, count);
    }
 
};

67.给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],…,k[m]。请问k[0]xk[1]x…xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

动态规划问题,贴一个大佬的思路:

#include <iostream>
#include <cmath>
 
using namespace std;
 
/**
 * 题目分析:
 * 先举几个例子,可以看出规律来。
 * 4 : 2*2
 * 5 : 2*3
 * 6 : 3*3
 * 7 : 2*2*3 或者4*3
 * 8 : 2*3*3
 * 9 : 3*3*3
 * 10:2*2*3*3 或者4*3*3
 * 11:2*3*3*3
 * 12:3*3*3*3
 * 13:2*2*3*3*3 或者4*3*3*3
 *
 * 下面是分析:
 * 首先判断k[0]到k[m]可能有哪些数字,实际上只可能是2或者3。
 * 当然也可能有4,但是4=2*2,我们就简单些不考虑了。
 * 5<2*3,6<3*3,比6更大的数字我们就更不用考虑了,肯定要继续分。
 * 其次看2和3的数量,2的数量肯定小于3个,为什么呢?因为2*2*2<3*3,那么题目就简单了。
 * 直接用n除以3,根据得到的余数判断是一个2还是两个2还是没有2就行了。
 * 由于题目规定m>1,所以2只能是1*1,3只能是2*1,这两个特殊情况直接返回就行了。
 *
 * 乘方运算的复杂度为:O(log n),用动态规划来做会耗时比较多。
 */
long long n_max_3(long long n) {
    if (n == 2) {
        return 1;
    }
    if (n == 3) {
        return 2;
    }
    long long x = n % 3;
    long long y = n / 3;
    if (x == 0) {
        return pow(3, y);
    } else if (x == 1) {
        return 2 * 2 * (long long) pow(3, y - 1);
    } else {
        return 2 * (long long) pow(3, y);
    }
}
 
//给你一根长度为n的绳子,请把绳子剪成m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],...,k[m]。请问k[0]xk[1]x...xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
//
//输入描述:
//输入一个数n,意义见题面。(2 <= n <= 100)
//
//
//输出描述:
//输出答案。
//示例1
//输入
//8
//输出
//18
int main() {
    long long n = 0;
    cin >> n;
    cout << n_max_3(n) << endl;
    return 0;
}

整理一下代码为:

class Solution {
public:
    int cutRope(int number) {
        if(number==2)
            return 1;
        if(number==3)
            return 2;
         
        int x=number%3;
        int y=number/3;
        if(x==0)
            return pow(3,y);
        else if(x==1)
            return 2*2*(int)pow(3,y-1);
        else
            return 2*pow(3,y);
    }
};

总结:

总算是全“刷"完了,然而这第一遍还是以借鉴别人的修改为主。以后还会经常自己回来看看,记录蒟蒻的成长!

发布了36 篇原创文章 · 获赞 8 · 访问量 1566

猜你喜欢

转载自blog.csdn.net/weixin_43619346/article/details/104345961