剑指offer刷题笔记-篇1 起始

本文介绍了多种数据结构和算法问题的解决方案,包括数组重复数字的查找、二维数组中的查找、字符串处理、链表操作、二叉树重建、斐波那契数列计算以及搜索和遍历策略。这些方法注重效率,通过巧妙的逻辑减少了时间和空间复杂度。
摘要由CSDN通过智能技术生成

剑指offer刷题笔记

前言

主要刷题平台为 牛客网,部分题目使用 LeetCode 和 ACwing 作为辅助。每题均包含主要思路、详细注释、时间复杂度和空间复杂度分析,每题均是尽可能最佳的解决办法。

重复数字I

链接:

acwing:重复数字

newcode:重复数字

题目:

给定一个长度为 n 的整数数组 nums,数组中所有的数字都在 0∼n−1 的范围内。

数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。

请找出数组中任意一个重复的数字。

注意:如果某些数字不在 0∼n−1 的范围内,或数组中不包含重复数字,则返回 -1;

思路:

先将每个数字都放到对应的位置上,然后如果在放的过程中,发现该位置已经有数字了,那么就说明这个数字是多余的了

class Solution {
public:
    int duplicateInArray(vector<int>& nums) {
        //每个数放到对应的位置上,如 时间复杂度 O(1)
        
        int n=nums.size();
        for(int i=0;i<n;i++)
        {
            //如果超出数据范围
            if(nums[i]<0||nums[i]>=n)
            {
                return -1;
            }
            
        }
        
        for(int i=0;i<n;i++)
        {
            //将每个数字都放到对应的位置上
            while(i!=nums[i]&&nums[nums[i]]!=nums[i])
            {
                //如果该位置已经无法交换,就是 该位置上的数字已经是 对应的数字了
                swap(nums[i],nums[nums[i]]);
                
            }
            //当前位置已经有对应数字了
            if(nums[i]!=i)
            {
                return nums[i];
            }
        }
        return -1;
    }
};

时间复杂度 O ( n ) O(n) O(n),额外空间复杂度 O ( 1 ) O(1) O(1)

重复数字II

ACwing重复数字II

LeetCode 寻找重复数字

题目:

给定一个长度为 n+1的数组nums,数组中所有的数均在 1∼n 的范围内,其中 n≥1。

请找出数组中任意一个重复的数,但不能修改输入的数组。

思路:

有n+1个数字,但是数字范围在1~n,所以一定会有一个重复的数字,我们要 做的工作就是找到这个重复数字,
我们采用分治的思想,将每个数的取值的区间[1, n]划分成[1, n/2]和[n/2+1, n]两个子区间,然后分别统计两个区间中数的个数。
注意这里的区间是指 数的取值范围,而不是 数组下标。
划分之后,左右两个区间里一定至少存在一个区间,区间中数的个数大于区间长度。

class Solution {
public:
    int duplicateInArray(vector<int>& nums) {
        //[l,mid] 和 [mid+1,r]
        int l=1,r=nums.size()-1;
        
        //整数二分 
        while(l<r)
        {
            int mid=l+r>>1;
            int s=0;
            //统计左边数字数量
            for(auto x:nums)
            {
                //s += x >= l && x <= mid;
                if (x >= l && x <= mid)
                    s ++ ;
                
            }
            if(s>mid-l+1)
            {
                //重复数字在左半边
                r=mid;
            }
            else
            {
                l=mid+1;
            }
        }
        return r;
    }
};

时间复杂度:每次会将区间长度缩小一半,一共会缩小 O ( l o g n ) O(logn) O(logn) 次。每次统计两个子区间中的数时需要遍历整个数组,时间复杂度是 O ( n ) O(n) O(n)。所以总时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)
空间复杂度:代码中没有用到额外的数组,所以额外的空间复杂度是 O ( 1 ) O(1) O(1)

二维数组中的查找

acwing二维数组中的查找

leetcode:二维数组中的查找

newcode:二维数组中的查找

题目:

在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。

请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

数据范围:二维数组中元素个数范围 [0,1000]

思路:

暴力搜索的时间复杂度是 O ( n 2 ) O(n^2) O(n2), 由于数据不是很高,所以可以通过:

class Solution {
public:
    bool searchArray(vector<vector<int>> array, int target) {
        for(int i=0;i<array.size();i++)
        {
            for(int j=0;j<array[i].size();j++)
            {
                if(array[i][j]==target)
                {
                    return true;
                }
            }
        }
        return false;
    }
};

这里是有时间复杂度可以达到 O ( n ) O(n) O(n),因为题目中的二维数组 是有单调性的,所以我们可以从 右上角 的那个元素入手,如果 target 比右上角的元素大,则说明 target 不在该行,如果target比右上角元素小,则说明target不在该列,就可以实现要么排除掉 一行,要么排除掉一列

class Solution {
public:
    bool searchArray(vector<vector<int>> array, int target) {
        if(array.empty()||array[0].empty())
        {
            return false;
        }
        
        int i=0,j=array[0].size()-1;
        while(j>=0&&i<array.size())
        {
            if(array[i][j]>target)
            {
                j--;
            }
            else if(array[i][j]<target)
            {
                i++;
            }
            else
            {
                return true;
            }
        }
        return false;
    }
};

替换空格

newcode: 替换空格

leetcode: 替换空格

题目:

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

思路:

比较简单,使用额外空间:

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param s string字符串 
     * @return string字符串
     */
    string replaceSpace(string s) {
        // write code here
        string result;
        for(auto x:s)
        {
            if(x==' ')
            {
                result+="%20";
            }
            else
            {
                result+=x;
            }
        }
        return result;
    }
};

用时间换空间,在原数组上操作,不使用额外的内存空间

class Solution {
public:

    string replaceSpace(string s) {
        //使用从后向前填充,不用申请新的数组
        //避免了从前向后填充元素 需要每次将添加元素之后的所有元素 向后移动

        //统计空格个数
        int count=0;
        int sOldSize=s.size();
        for (int i=0;i<s.size();i++)
        {
            if(s[i]==' ')
            {
                count++;
            }
        }

        //扩容字符串大小,替换后的大小
        //因为本来就有一个空格,所以只要再多增加两个空格的大小,就可以放下这个字符了
        s.resize(s.size()+count*2);
        int sNewSize=s.size();

        //从后向前替换为 “%20”
        for (int i=sNewSize-1,j=sOldSize-1;j<i;i--,j--)
        {
            //i 是新字符串(扩容后,后面全部都是空格)的最后一个位置, j 是旧字符串(扩容前)的最后一个位置,现在就是把就字符串依次后移,如果遇到空格,就替换为 %20
            if (s[j]!=' ')
            {
                //将字符串中的字符后移
                s[i]=s[j];
            }
            else
            {
                s[i]='0';
                s[i-1]='2';
                s[i-2]='%';
                i-=2;
            }
        }
        return s;


    }
};

打印链表

链接:

newcode打印链表

acwing打印链表

题目:

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

思路:

如果要使用 容器的性质,可以很轻易的实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> reversePrint(ListNode* head) {
        vector<int> result;
        ListNode* cur=head;
        while(cur!=nullptr)
        {
            result.push_back(cur->val);
            cur=cur->next;
        }
        //反转容器中的数据
        return vector<int>(result.rbegin(),result.rend());
    }
};

*重建二叉树

链接:重建二叉树

题目:

给定节点数为 n 的二叉树的前序遍历和中序遍历结果,请重建出该二叉树并返回它的头结点。

例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建出如下图所示。

img

/**
 * Definition for binary tree
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    
    TreeNode* traversal(vector<int>& vin,int vinBegin,int vinEnd,vector<int>& pre,int preBegin,int preEnd)
    {
        if(preBegin==preEnd)
        {
            return nullptr;
        }
        TreeNode* root=new TreeNode(pre[preBegin]);
        
        //在 中序数组中寻找 根节点
        int delimeterIndex=0;
        for(delimeterIndex=vinBegin;delimeterIndex<vinEnd;delimeterIndex++)
        {
            if(vin[delimeterIndex]==pre[preBegin])
            {
                break;
            }
            
        }
        
        //切割中序数组,因为要使用递归,所以一定要使用函数内的变量
        int leftVinBegin=vinBegin;    //左闭右开
        int leftVinEnd=delimeterIndex;
        
        int rightVinBegin=delimeterIndex+1;
        int rightVinEnd=vinEnd;
        
        //切割前序数组
        int leftPreBegin=preBegin+1;    //左闭右开
        int leftPreEnd=preBegin+1+(delimeterIndex-vinBegin);    //终止位置为 起始位置+中序左区间大小
        
        int rightPreBegin=preBegin+1+(delimeterIndex-vinBegin);
        int rightPreEnd=preEnd;
        
        root->left=traversal(vin, leftVinBegin, leftVinEnd,pre,leftPreBegin,leftPreEnd);
        root->right=traversal(vin, rightVinBegin, rightVinEnd,pre,rightPreBegin,rightPreEnd);
        
        return root;
    }
    
    TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
        
        if(pre.size()==0||vin.size()==0)
        {
            return nullptr;
        }
        return traversal(vin,0,vin.size(),pre,0,pre.size());
        
    }
};

二叉树的下一个结点

下一个结点

题目:

给定一个二叉树其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的next指针。下图为一棵有9个节点的二叉树。树中从父节点指向子节点的指针用实线表示,从子节点指向父节点的用虚线表示

最重要的就是要分开讨论不同的情况,实现所有要求,一定要记清 中序遍历的顺序:左-中-右
如果该节点有右子树,那么下一个结点就是右子树中的 最左边的 左孩子
如果该节点没有右子树,那么下一个节点就要看父节点了,如果当前节点是父节点的左孩子,那么下一个结点就是 当前节点的 父节点(不一定是直接父节点)

/*
struct TreeLinkNode {
    int val;
    struct TreeLinkNode *left;
    struct TreeLinkNode *right;
    struct TreeLinkNode *next;
    TreeLinkNode(int x) :val(x), left(NULL), right(NULL), next(NULL) {
        
    }
};
*/
class Solution {
public:
    TreeLinkNode* GetNext(TreeLinkNode* pNode) {
        //根据中序遍历,如果有右子树,那么右子树的左孩子(最左边的左孩子)J就是下一额节点
        if(pNode->right)
        {
            pNode=pNode->right;
            //右子树的最左边的孩子
            while(pNode->left)
            {
                pNode=pNode->left;
            }
            return pNode;
        }
        //直到找到当前节点 是 父节点 的左孩子
//         while(pNode->next&&pNode==pNode->next->right)
//         {
//             //第一个当前节点是父节点左孩子的节点
//             pNode=pNode->next;
//         }
//         return pNode->next;
        
        //没有右子树,只有左子树
        while(pNode->next)
        {
            //是父节点的左子树,那么下一个结点就是 父节点
            if(pNode==pNode->next->left)
            {
                return pNode->next;
            }
            //不是父节点的左子树就继续找父节点
            pNode=pNode->next;
        }
        //没有父节点
        return nullptr;
        
    }
};

两个栈实现队列

两个栈实现队列

题目:

用两个栈来实现一个队列,使用n个元素来完成 n 次在队列尾部插入整数(push)和n次在队列头部删除整数(pop)的功能。 队列中的元素为int类型。保证操作合法,即保证pop操作时队列内已有元素。

数据范围: n≤1000

要求:存储n个元素的空间复杂度为 O(n) ,插入与删除的时间复杂度都是 O(1)

思路:

一定要先想清楚再下手,我们要用栈(先进后出)实现队列(先进先出),一个 栈 肯定是完不成的,必须要使用两个栈来实现,我们可选择将所有的 入队 都放在stack1 中,然后如果要 出队,就可以将 stack1 中的内容 再放到 stack2 中,这样放入stack2 中的 从顶到底的顺序,就是 队列 出队的顺序了,只要 stack2不是空的,就可以确保 stack2的顺序是出队顺序,如果stack2空了,就继续将 stack1 中的元素放入 stack2

class Solution
{
public:
    void push(int node) {
        //新数据都放入 stack1 中
        stack1.push(node);
    }

    int pop() {
        //由于数据都是放在 stack1 中的,我们要进行出队,就是stack1 中的栈底元素,我们将 stack1 中的元素全部发放到
        //stack2 中之后,stack2的遍历顺序就是 队列的出列顺序了
        if(stack2.empty())
        {
            while(!stack1.empty())
            {
                stack2.push(stack1.top());
                stack1.pop();
            
            }
           
        }
        //元素出栈之后,要在栈中弹出
        int ret=stack2.top();
        stack2.pop();
        return ret;
     
    }

private:
    stack<int> stack1;
    stack<int> stack2;
};

斐波那契数列

斐波那契数列

题目:

大家都知道斐波那契数列,现在要求输入一个正整数 n ,请你输出斐波那契数列的第 n 项。

斐波那契数列是一个满足 f i b ( x ) = { 1 x = 1 , 2 f i b ( x − 1 ) + f i b ( x − 2 ) x > 2 fib(x)=\left\{ \begin{array}{rcl} 1 & {x=1,2}\\ fib(x-1)+fib(x-2) &{x>2}\\ \end{array} \right. fib(x)={1fib(x1)+fib(x2)x=1,2x>2的数列

数据范围: 1 ≤ n ≤ 40 1\leq n\leq 40 1n40

要求:空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n ) O(n) O(n) ,本题也有时间复杂度 O ( l o g n ) O(logn) O(logn) 的解法

思路:

这个做法是我目前看到的最优的做法了,时间复杂度为 O ( n ) O(n) O(n), 空间复杂度为 O ( 1 ) O(1) O(1), 因为每一个 斐波那契数字都 只与前面的两个数字有关,所以可以只记录前面的两个,实现降低 空间复杂度(空间复杂度为 O ( n ) O(n) O(n)的做法就是使用 动态规划的做法了)
递归的写法就不再演示了,递归法:时间复杂度: O ( 2 n ) O(2^n) O(2n) 空间复杂度:递归栈的空间

class Solution {
public:
    int Fibonacci(int n) {
        /*
        if(n==1||n==2)
        {
            return 1;
        }
        */
        //因为求解只涉及到三个变量,所以使用三个变量来实现
        int a=0,b=1; 
        //对于每一个斐波那契数,只和他前面的两个有关,所以可以进一步压缩空间
        while(n--)
        {
            //a b  交替更新
            int c=a+b;
            a=b;
            b=c;
        }
        return a;
        
    }
};

空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n ) O(n) O(n)

动态规划做法:可以根据数据范围开辟对应大小的 dp数组,虽然时间复杂度可以达到要求,但是 空间复杂度明显没有上一个更小

class Solution {
public:
    int Fibonacci(int n) {
        //根据数据范围确定 dp数组的大小
        int dp[40]={0};
        dp[0]=0;
        dp[1]=1;
        dp[2]=1;
        //从第3项开始,要包含最后一项
        for(int i=3;i<=n;i++)
        {
            dp[i]=dp[i-1]+dp[i-2];
        }
        return dp[n];
    }
};

旋转数组

acwing题解

newcode:旋转数组

题目:

有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。

数据范围: 1 ≤ n ≤ 10000 1 \le n \le 10000 1n10000,数组中任意元素的值: 0 ≤ v a l ≤ 100000 0 \le val \le 100000 0val100000

要求:空间复杂度: O ( 1 ) O(1) O(1) ,时间复杂度: O ( l o g n ) O(logn) O(logn)

思路:

直接使用二分查找

class Solution {
public:
    int minNumberInRotateArray(vector<int> rotateArray) {
        //二分查找
        if(rotateArray.size()==0)
        {
            return 0;
        }
        
        int left=0,right=rotateArray.size()-1;
        while(left<right)
        {
            if(rotateArray[left]<rotateArray[right])
            {
                return rotateArray[left];
            }
            int mid=left+right>>1;
            //int mid=left+((right-left))>>1;
            if(rotateArray[right]<rotateArray[mid])
            {
                left=mid+1;
            }
            else if(rotateArray[right]>rotateArray[mid])
            {
                right=mid;
            }
            else 
            { 
                //不能确定答案在左边还是右边,那么就让last = last - 1;慢慢缩少区间,同时也不会错过答案
                --right;
            }
            
        }
        return rotateArray[left];
    }
};

空间复杂度: O ( 1 ) O(1) O(1) ,时间复杂度: O ( l o g n ) O(logn) O(logn)

矩阵中的路径

矩阵中的路径

题目:

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

数据范围: 0 ≤ n , m ≤ 20   , 1 ≤ l e n ≤ 25 0 \le n,m \le 20\ ,1\le len \le 25 0n,m20 ,1len25

进阶:时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( n 2 ) O(n^2 ) O(n2)

思路:

深度优先遍历的经典题目
枚举单词的起点,然后依次枚举单词的每个字母。
过程中需要将已经使用过的字母改成一个特殊字母,以避免重复使用字符。

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param matrix char字符型vector<vector<>> 
     * @param word string字符串 
     * @return bool布尔型
     */
    bool dfs(vector<vector<char>>& matrix,string &word,char u,int x,int y)
    {
        //该路径上的字符不一致
        if(matrix[x][y]!=word[u])
        {
            return false;
        }
        //这两个判断的先后顺序不能颠倒
        //找到该字符串,当前字符匹配且当前为最后一个字符时,直接返回true
        if(u==word.size()-1)
        {
            return true;
        }
        
        //上右下左  横为y,纵为x
        int idx[4]={-1,0,1,0};
        int idy[4]={0,1,0,-1};
        //记录当前值
        char t=matrix[x][y];
        //修改当前坐标的值,防止重复使用
        matrix[x][y]='*';
        for(int i=0;i<4;i++)
        {
            //尝试四个方向
            int a=x+idx[i];
            int b=y+idy[i];
            
            if(a>=0&&a<matrix.size()&&b>=0&&b<matrix[a].size())
            {
                if(dfs(matrix, word, u+1, a, b))
                {
                    return true;
                }
                
            }
        }
        //恢复现场
        matrix[x][y]=t;
        return false;
    }
    bool hasPath(vector<vector<char> >& matrix, string word) {
        // write code here
        for(int i=0;i<matrix.size();i++)
        {
            for(int j=0;j<matrix[i].size();j++)
            {
                if(dfs(matrix,word,0,i,j))
                {
                    return true;
                }
                
            }
        }
        return false;
    }
};

时间复杂度分析:单词起点一共有 n 2 n^2 n2 个,单词的每个字母一共有上下左右四个方向可以选择,但由于不能走回头路,所以除了单词首字母外,仅有三种选择。所以总时间复杂度是 O ( n 2 3 k ) O(n^23^k) O(n23k)

*机器人的运动范围

机器人的运动范围

题目:

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

思路:

宽度优先遍历的经典题目
要注意判断障碍物,有些格子是不能走的

class Solution {
public:
    int get_signal_sum(int x)
    {
        int s=0;
        while(x)
        {
            s+=x%10;
            x/=10;
        }
        return s;
    }
    int get_sum(pair<int, int> p)
    {
        return get_signal_sum(p.first)+get_signal_sum(p.second);
    }
    int movingCount(int threshold, int rows, int cols) {
        int res=0;
        if(rows==0||cols==0)
        {
            return 0;
        }
        
        //默认所有格子都是没有走过的
        vector<vector<bool>> st(rows,vector<bool>(cols));
        queue<pair<int, int>> q;    //宽度优先搜索使用的队列
        
        //也可以使用 make_pair
        q.push({0,0});
        
        //走的方向: 纵为x 横为y  上右下左
        int dx[4]={-1,0,1,0};
        int dy[4]={0,1,0,-1};
        //循环放入
        while(q.size())
        {
            auto t=q.front();
            q.pop();
            
            //如果不符合条件, 就不走这条路
            if(get_sum(t)>threshold||st[t.first][t.second]==true)
            {
                continue;
            }
            res++;
            st[t.first][t.second]=true;    //表示走过了
            
            //走的方向
            for(int i=0;i<4;i++)
            {
                int x=t.first+dx[i];
                int y=t.second+dy[i];
                
                //判断是否合法的坐标
                if(x>=0&&x<rows&&y>=0&&y<cols)
                {
                    q.push({x,y});
                }
            }
        }
        return res;
        
    }
};

每个节点最多只会入队一次,所以时间复杂度不会超过方格中的节点个数。
最坏情况下会遍历方格中的所有点,所以时间复杂度就是 O(nm)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值