leetcode剑指offer刷题笔记【第一周】

本文分享了使用C++进行算法刷题的体验,从双栈实现队列和最小值栈,到链表的反转和复杂链表复制,再到字符串处理和数组问题,最后涉及二叉树的层次遍历和子结构判断。作者强调了C++的便利性和哈希表的重要性,并通过递归解决二叉树问题。
摘要由CSDN通过智能技术生成

之前一直在用C刷题,但很多题目明显麻烦,也导致自己对leetcode比较抗拒,又懒于学习C++,java之类的,所以刷题一直拖着,现在终于下定决心用C++来刷题,发现真的好用,很多东西有现成的,可以更专注于学习算法,因此希望可以坚持下去。不会的题都是参考leetcode题解大佬Krahets的。

day1

双栈实现队列、包含min的栈

首先把C++中这些类常用的函数记录一下。

#include <stack>//栈
stack<int> st;
st.push(elem);
st.pop();
st.emplace(args);//用参数args构造一个元素并添加进栈顶
st.top();
st.empty();
st.size();

对于双栈实现队列,只需要设置两个栈A,B,进栈只需入栈A,出栈时,如果B栈非空,则popB栈,若A,B都空,返回-1,如果B为空,则将A中元素出栈入B栈,再popB栈。

class CQueue {
public:
    stack<int> inStack, outStack;//in是入栈、out是出栈
    CQueue() {
    }
    void appendTail(int value) {//整体入队则直接在push入in栈
        inStack.push(value);
    }
    int deleteHead() {
        if(!outStack.empty()){//若out非空,则记录栈头元素再出栈
            int value = outStack.top();
            outStack.pop();
            return value;
        }
        if(inStack.empty()) return -1;
        while(!inStack.empty()){
            outStack.push(inStack.top());
            inStack.pop();
        }
        int value= outStack.top();
        outStack.pop();
        return value;
    }
};

包含min函数的栈,同样需要两个栈,A、B为空时,第一个元素同时入A,B栈,对于后面的元素,若必此时B栈顶元素小,则入A,B栈,否则只入A栈,保持B中元素栈顶是最小的,此时,若输出栈中最小元素,则只需要输出B栈顶元素。(A是一个完整的栈,B是用来记录栈中最小的元素的。

class MinStack {
    stack<int> Astack, Bstack;
public:
    /** initialize your data structure here. */
    MinStack() {
        
    }
    
    void push(int x) {
        Astack.push(x);
        if(Bstack.empty()||Bstack.top()>=x) Bstack.push(x);
    }
    
    void pop() {
        int temp=Astack.top();//需先将A栈pop,比较B栈顶元素与此相等时,Bpop
        Astack.pop();
        if(temp==Bstack.top()){
            Bstack.pop();
        }
    }
    
    int top() {
        return Astack.top();
    }
    
    int min() {
        return Bstack.top();
    }
};

day2

反转链表、从尾到头打印链表、复制复杂链表

反转链表408考研必须掌握的,因此比较熟悉。

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode *q=NULL;
        ListNode *p=head;
        ListNode *r;
        while(p){
            r=p->next;
            p->next=q;
            q=p;
            p=r;
        }  
        return q;//此时q指向新的表头,即原来的表尾
    }
};

对于从尾到头打印链表,本题要返回一个vector,因此可以先逆置再添加到vector也可以利用栈先将链表元素入栈再添加到vector。
同样先将vector记录一下

#include <vector>
vector<int> array; //创建vector对象
array.push_back(a); //尾部插入数字a
array.pop_back();//删除向量最后一个元素
array.at(i); //使用下标访问元素
vector<int>::iterator it;//使用迭代器访问元素
for(it=array.begin();it!=array.end();i++)
	cout<<*it<<endl;
array.insert(array.begin()+i,a);//在第i+1个元素前面插入a;
array.erase();//删除元素
array.erase(a,b);//删除区间
array.size();//向量大小
array.clear()//向量清空
array.empty()//判空
array.back();//最后一个元素
array.front();//第一个元素

这里使用栈

class Solution {
public:
    vector<int> reversePrint(ListNode* head) {
        stack<int> s;
        vector<int> res;
        ListNode *p=head;
        while(p){
            s.push(p->val);//入栈
            p=p->next;
        }
        while(!s.empty()){//栈非空时加到vector后面
            res.push_back(s.top());
            s.pop();
        }
        return res;

    }
};

对于复制复杂链表,这是我第一次正式使用哈希表,虽然408学了原理,但实际也没怎么用过。本题使用unordered_map构建原结点和新结点的映射关系,因为存在了random这个指针,所以只能先把每个结点位置确定好后,再来连线。不能像构建普通链表那样一边创造新结点一边就连线。

class Solution {
public:
    Node* copyRandomList(Node* head) {
        if(head==nullptr) return nullptr;//若头结点为空则返回空
        Node *cur =head; //cur指向原链表表头
        unordered_map<Node*, Node*> map;//定义一个map用来存原结点和新结点的映射
        while(cur!=nullptr){
            map[cur]=new Node(cur->val);//构建哈希表每个cur在map里对应一个新结点
            cur=cur->next;
        }
        cur = head;
        while(cur!=nullptr){//连线
            map[cur]->next = map[cur->next];
            map[cur]->random = map[cur->random];
            cur=cur->next;
        }
        return map[head];
    }
};

哈希表确实很方便,学完组成原理对地址等知识有了理解后学习C之类的语言确实不会太一知半解了。

day3 字符串替换空格,字符串左旋转

C中常用的看的下面这个博客。
C++中字符串类常用操作
C++中字符串是可变长的,因此对于修改字符串可以在原字符串上进行,而java,python属于不可变长的,只能创造一个新的字符串。本题参考了C++的解答,也是很巧妙。
通过统计空格的个数,增加字符串的大小,再从字符串后往前修改。

class Solution {
public:
    string replaceSpace(string s) {
        int count=0, len=s.size();
        for(char c:s){
            if(c==' ') count++;  //统计空格的数量
        }
        s.resize(len+2*count);  //每个空格带来2个容量的扩充
        for(int i=len-1,j=s.size()-1;i<j;i--,j--){
            if(s[i]!=' ') 
            s[j]=s[i];
            else{
                s[j-2]='%';
                s[j-1]='2';
                s[j]='0';
                j-=2;
            }

        }
        return s;
    }
};

字符串左旋转属于是408基础中的基础了,因此可以自己写出,时间复杂度高,空间复杂度低。

class Solution {
public:
    void Reverse(string &s,int low,int high){
        int i=low,j=high-1;
        while(i<j){
            char temp=s[i];
            s[i]=s[j];
            s[j]=temp;
            i++;
            j--;
        }
    }

    string reverseLeftWords(string s, int n) {
        int i=0,j=n,k=s.size();
        Reverse(s,i,j);
        Reverse(s,j,k);
        Reverse(s,i,k);
        return s;
    }
};

day4 数组中重复的数字、排序数组中重复的数字、0~n-1中缺失的数字

对于重复的数字这题,也算是408数据结构经典题了,数字比数组长度小,只需重设一个数组计数即可,如下

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        int j=0;
        int n= nums.size();
        vector<int> count(n,0);
        for(int i=0;i<n;i++){
            count[nums[i]]++;
        }
        for(j;j<n;j++){
            if(count[j]>1)
                break;
        }
        return j;
        if(j>=n) return -1;
    }

};

我也想了用哈希表,但奈何对哈希不熟,看了题解后发现的确可以而且更简单,如下

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        unordered_map<int, bool> map;
        for(int num:nums){
            if(map[num]) return num; //若已出现过,直接返回此时的num
            map[num]=true;
        }
        return -1;
    }

};

升序数组中查找目标出现的次数,最易想到的就是从头到尾遍历,如下

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int count = 0;
        for(int i=0;i<nums.size();i++){
            if(nums[i]==target)
                count++;
            if(nums[i]>target)
                break;
        }
        return count;
    }
};

但这样浪费了升序的条件,而升序适合使用二分查找,本题可通过二分查找找到数组中等于target的下边界和上边界,如下

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int i=0,j=nums.size()-1;
        while(i<=j){    //寻找有边界
            int m=i+(j-i)/2;
            if(nums[m]<=target) 
                i=m+1;
            else
                j=m-1;
        }
        int right=j;   
        i=0,j=nums.size()-1;
        while(i<=j){    //寻找左边界
            int m=i+(j-i)/2;
            if(nums[m]>=target)
                j=m-1;
            else
                i=m+1;
        }
        int left=i;
        return right-left+1;
    }
};

第三道同样是升序数组的查找问题,应直接想二分法,而我依然首先选择了遍历,

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int k=0;
        int n=nums.size()+1;
        vector<int> count(n,0);
        for(int i=0;i<nums.size();i++){
            count[nums[i]]++;
        }
        for(k;k<n;k++){
            if(count[k]==0)
                break;
        }
        return k;
    }
};

毫无疑问时间复杂度和空间复杂度都很高。

其实就是找到第一个下标与数组内容不同的那个下标,因此可通过二分法将其分为两个数组。代码如下。

class Solution {
public:
    int missingNumber(vector<int>& nums) {
      int i=0, j=nums.size()-1;
      while(i<=j){
          int m = i+(j-i)/2;
          if(m==nums[m]) i=m+1; 
          else j=m-1;
      }
      return i;
    }
};

day5

二维数组中的查找、旋转数组的最小数字、第一个只出现一次的字符

二维数组的查找同样是行列升序的数组,想到二分法,每行都使用一次二分,时间复杂度 O ( n l o g 2 m ) O(nlog_2m) O(nlog2m), 我的代码如下

class Solution {
public:
    bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
        if(matrix.size()==0)
            return false;
        int n=matrix.size();
        int m=matrix[0].size();
        for(int i=0;i<n;i++){
            int low=0,high=m-1;
            while(low<=high){
                int mid= low+(high-low)/2;
                if(matrix[i][mid]==target)
                    return true;
                else if( matrix[i][mid]<=target)
                    low=mid+1;
                else
                    high=mid-1;
            }
        }
        return false;
    }
};

跟官方题解比起来很冗长,他似乎用了很多C++内置的一些特殊用法,我这其实算是C语言hh。另外一种方法似乎是可以,从左下角往右上角遍历,若此时的数大于target,则令行-1,若小于target则令列+1。时间复杂度 O ( m + n ) O(m+n) O(m+n),代码如下:

class Solution {
public:
    bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
        int i=matrix.size()-1; //行数
        int j=0; //列数
        while(i>=0&&j<matrix[0].size()){
            if(matrix[i][j]>target)
                i--;
            else if(matrix[i][j]<target)
                j++;
            else
                return true;
        }
        return false;
    }
};

第二题旋转数组的最小数字,本题其实跟之前有一题很像,主要就是分成两个数组,找到右数组的起始,用二分法,但本题其实还是比较麻烦,因为当mid的值与端点值相同时,无法确定最小的数在左边还是右边,这也是我一直出错的地方,考虑的不太周到,看了题解发现,当无法判断时,只能缩小一点范围,也就是令j=j-1,重新判断。完整代码如下。

class Solution {
public:
    int minArray(vector<int>& numbers) {
        int i=0,j=numbers.size()-1;
        while(i<j){
            int mid=i+(j-i)/2;
            if(numbers[mid]>numbers[j])
                i=mid+1;
            else if(numbers[mid]<numbers[j])
                j=mid;
            else j--;
        }
        return numbers[i];
    }
};

第三题显然使用哈希,但还是不熟悉,看了题解才知道可以使用bool来表示

class Solution {
public:
    char firstUniqChar(string s) {
        unordered_map<char, bool> map;
        for(char c:s)
            map[c] = map.find(c)==map.end(); //这里好像是一个固定用法,但对find和end函数都不熟悉
        for(char c:s)
            if(map[c])  return c;
        
        return ' ';
    }
};

day6 按层的顺序打印二叉树Ⅰ、Ⅱ、Ⅲ

第一题按行顺序打印结点数字,就是二叉树的层序遍历,用C++写很多东西现成的,按照考研备考时的思路成功完成,看来层序遍历已经拿下,就是用一个队列来辅助,代码如下。

class Solution {
public:
    vector<int> levelOrder(TreeNode* root) {
        vector<int> res;  //返回数组
        queue<TreeNode*> q;   //队列
        TreeNode* p;  //储存每次弹出的结点
        if(root)
            q.push(root); //根非空时,将根结点入队
        while(q.empty()==false){ //队列不空时执行循环
            p=q.front();   //将队首结点出队记作p
            res.push_back(p->val);  //将p的val写入res数组
            if(p->left)             //左孩子不空则左孩子入队
                q.push(p->left);
            if(p->right)            //右孩子不空则右孩子入队
                q.push(p->right);
            q.pop();                //弹出队首结点
        }
        return res;
    }
};

第二题需要返回一个二维数组,每一行是每层的元素,一直有个疑惑到底怎么样才能知道每一层的个数,题解巧妙使用了队列长度进行for循环,妙极了。代码如下。

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> res;
        queue<TreeNode*> q;
        if(root)
            q.push(root);
        while(q.empty()==false){
            vector<int> temp;  //临时的vector用来存储每一行上的数
            for(int i=q.size();i>0;i--){ //每一次循环结束后就将下一层的所有结点都入队了,
            						//此时通过while再次进入循环,得到的q.size就是这一层元素个数
                TreeNode* p=q.front();
                temp.push_back(p->val);
                if(p->left)			//剩下的步骤与题Ⅰ类似
                    q.push(p->left);
                if(p->right)
                    q.push(p->right);
                q.pop();
            }
            res.push_back(temp);
        }
        return res;
    }
};

第三题z字形输出每一行的元素,用了个CPP里面的reverse函数讨巧办法先通过了,但明显本意不是如此,再学习一下大佬的思路。

day7 树的子结构、二叉树的镜像、对称的二叉树

树这里的算法题大概多用递归,本题判断B是否是A的子结构,成立的条件只有三种,一种是A、B的根节点相同,从而B是A的子结构,另外B可能隐藏在A的左子树或右子树里,因此可以递归的查找A、A的左子树、A的右子树。代码如下

class Solution {
public:
    bool isSubStructure(TreeNode* A, TreeNode* B) {
        if(A==nullptr||B==nullptr)
            return false;
        return (recur(A,B)||isSubStructure(A->left,B)||isSubStructure(A->right,B));//真的三种条件,满足其一即可
    }

private:
    bool recur(TreeNode* A, TreeNode* B){ //用来判断A和B根节点相同情况下,B是否包含在A中
        if(B==nullptr) return true; //如果B是空指针,则说明B已经遍历完,返回真
        if(A==nullptr||A->val!=B->val) return false; //若B非空的情况下A空了,或者AB的值不等则false
        return (recur(A->left,B->left)&&recur(A->right,B->right));  //再对左右结点进行判断
    }
};

二叉树的镜像、同样是递归,我想自上而下递归,大佬的题解似乎是自下而上,理解的不太好,还是写了自上而下的。

class Solution {
public:
    TreeNode* mirrorTree(TreeNode* root) {
        exchangeLR(root);
        return root;
    }
private:
    void exchangeLR(TreeNode* root){
        if(root==nullptr)
            return;
        swap(root->left,root->right);  //交换左右结点
        exchangeLR(root->left); //递归左子树
        exchangeLR(root->right); //递归右子树
        }
};

第三题一开始准备先求出镜像再比较是否相同的,但判断条件始终写不对,只好看了题解,只需要对左右子树进行判断是否相等即可。代码如下。

class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        if(root == NULL) return true;
        return  reer(root->left, root->right);
    }
    bool reer(TreeNode* A,TreeNode* B)
    {
        if(A == NULL && B==NULL) return true;
        if(A == NULL || B==NULL || A->val != B->val) return false;
        return reer(A->right, B->left)&&reer(A->left,B->right);
    }
};

递归出口的理解一直不深刻导致递归一看就会一写就寄。只能多学习吧。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值