《剑指offer》

14. 不修改数组找出重复的数字

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

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

样例

给定 nums = [2, 3, 5, 4, 3, 2, 6, 7]。

返回 2 或 3。

思考题:如果只能使用 O(1) 的额外空间,该怎么做呢?

class Solution {
public:
    int duplicateInArray(vector<int>& nums) {
        int l=1,r=nums.size()-1,ans;//[l,mid],[mid+1,r]
        while(l<=r){
            int mid=(l+r)/2;
            int s=0;
            for(auto x: nums){
                s+= x<=mid&&x>=l;
            }
            if(s>mid-l+1){
                ans=mid;
                r=mid-1;
            }else {
                l=mid+1;
            }
        }
        return ans;
    }
};

说明:时间复杂度O(nlogn)(二分O(logn),二分的while循环里面遍历nums,O(n)),空间复杂度O(1)(没格外开新数组) 

根据抽屉原理,范围1~n的数放在n+1个坑里,一定有至少两个数一样。依据分治思想。

二分的左边界是1,因为题目说数的范围在1~n。

15. 二维数组中的查找

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

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

样例

输入数组:

[

  [1,2,8,9],

  [2,4,9,12],

  [4,7,10,13],

  [6,8,11,15]

]

如果输入查找数值为7,则返回true,如果输入查找数值为5,则返回false。

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( i<array.size() && j>=0 ){
            if(array[i][j]==target)return true;
            if(array[i][j]>target) --j;
            else ++i;
        }
        return false;
    }
};

说明:注意到矩阵右上角元素的特殊性,每次可以筛掉一行或一列,时间复杂度O(n)。

17. 从尾到头打印链表

输入一个链表的头结点,按照 从尾到头 的顺序返回节点的值。

返回的结果用数组存储。

样例

输入:[2, 3, 5]

返回:[5, 3, 2]

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> printListReversingly(ListNode* head) {
        vector<int>ans;
        while(head){//当head不为空
            ans.push_back(head->val);
            head=head->next;
        }
        return vector<int>(ans.rbegin(),ans.rend());//构造一个新的vector,从尾到头遍历ans,用遍历的结果初始化新vector
    }
};

18. 重建二叉树

输入一棵二叉树前序遍历和中序遍历的结果,请重建该二叉树。

注意:

二叉树中每个节点的值都互不相同;

输入的前序遍历和中序遍历一定合法;

样例

给定:

前序遍历是:[3, 9, 20, 15, 7]

中序遍历是:[9, 3, 15, 20, 7]

返回:[3, 9, 20, null, null, 15, 7, null, null, null, null]    //注:输出采用二叉树的层序遍历

返回的二叉树如下所示:

    3

   / \

  9  20

    /  \

   15   7

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<int>preorder,inorder;
    map<int,int>hash;//存储元素在中序遍历中的位置(快速找根)
    TreeNode* buildTree(vector<int>& _preorder, vector<int>& _inorder) {
        preorder=_preorder,inorder=_inorder;
        for(int i=0;i<inorder.size();++i) hash[inorder[i]]=i;//inorder各元素的位置
        return dfs(0,preorder.size()-1,0,inorder.size()-1);
    }
    TreeNode* dfs(int pl,int pr,int il,int ir){
    //前序遍历左边界,前右,中序遍历左边界,中右,都是闭区间
        if(pl>pr)return nullptr;//
        auto root=new TreeNode(preorder[pl]);//new一个root,root是前序遍历左边界
        int k=hash[root->val];//根在中序遍历中的位置
        auto left=dfs(pl+1,pl+k-il,il,k-1);
    /*左子树的前左、前右、中左、中右分别是
    this的pl+1(pl是根),pl+1+k-il-1,il,k-1*/
        auto right=dfs(pl+k-il+1,pr,k+1,ir);
        root->left=left,root->right=right;
        return root;
    }
};

说明:采用分治的思想。

前序遍历preorder是“根左右”,中序遍历inorder是“左根右”。

第1棵树(第1层):preorder的第1个元素是它的根,在inorder中找到这个根的位置,根的左边是它的左子树,根的右边是它的右子树。再依据这个规则分别处理它的左子树、右子树即可。

如何在inorder中快速找到根的位置?开一个map(哈希表)处理一下即可。

dfs的区间容易弄错,在脑海中想象下面这个图就可以了:

preorder: [根][左左左左][右右右右]

inorder:   [左左左左][根][右右右右]

19. 二叉树的下一个节点

给定一棵二叉树的其中一个节点,请找出中序遍历序列的下一个节点。

(求二叉树的后继。在二叉搜索树中,某个结点的后继是比它大的第一个数。)

注意:

如果给定的节点是中序遍历序列的最后一个,则返回空节点;

二叉树一定不为空,且给定的节点一定不是空节点;

样例

假定二叉树是:[2, 1, 3, null, null, null, null], 给出的是值等于2的节点。

则应返回值等于3的节点。

解释:该二叉树的结构如下,2的后继节点是3。

  2

 / \

1   3

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode *father;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL), father(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* inorderSuccessor(TreeNode* p) {
        if(p->right){
            p=p->right;
            while(p->left)p=p->left;
            return p;
        }
        while(p->father&&p==p->father->right) p=p->father;
        return p->father;
    }
};

说明:

中序遍历:“左根右”。

分两种情况:1.该结点有右子树,则该结点的后继是它右子树的最左边的结点。2.该结点(this)无右子树,则该结点的后继要沿着它的父亲结点一直往上找,当某个父亲结点是某个结点x的左儿子为止,this结点的后继就是x。

对于一个点求后继,时间复杂度O(h)(h是二叉树的高度,要么向上找要么向下找)

如果要求每个点的后继,时间复杂度O(n),试证明。

20. 用两个栈实现队列

请用栈实现一个队列,支持如下四种操作:

push(x) – 将元素x插到队尾;

pop() – 将队首的元素弹出,并返回该元素;

peek() – 返回队首元素;

empty() – 返回队列是否为空;

注意:

你只能使用栈的标准操作:push to top,peek/pop from top, size 和 is empty;

如果你选择的编程语言没有栈的标准库,你可以使用list或者deque等模拟栈的操作;

输入数据保证合法,例如,在队列为空时,不会进行pop或者peek等操作;

样例

MyQueue queue = new MyQueue();

queue.push(1);

queue.push(2);

queue.peek();  // returns 1

queue.pop();   // returns 1

queue.empty(); // returns false

class MyQueue {
public:
    /** Initialize your data structure here. */
    stack<int>stk,cache;
    MyQueue() {
        
    }
    
    /** Push element x to the back of queue. */
    void push(int x) {
        stk.push(x);
    }
    
    void COPY(stack<int>& a, stack<int>& b){
        while(a.size()){
            b.push(a.top());
            a.pop();
        }
    }
    
    /** Removes the element from in front of queue and returns that element. */
    int pop() {
        COPY(stk,cache);
        int res=cache.top();
        cache.pop();//注意pop要弹出,peek不用
        COPY(cache,stk);
        return res;
    }
    
    /** Get the front element. */
    int peek() {
        COPY(stk,cache);
        int res=cache.top();
        COPY(cache,stk);
        return res;
    }
    
    /** Returns whether the queue is empty. */
    bool empty() {
        return stk.empty();
    }
};

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue obj = MyQueue();
 * obj.push(x);
 * int param_2 = obj.pop();
 * int param_3 = obj.peek();
 * bool param_4 = obj.empty();
 */

21. 斐波那契数列

输入一个整数 nn ,求斐波那契数列的第 nn 项。

假定从0开始,第0项为0。(nn<=39)

样例

输入整数 n=5

返回 5

class Solution {
public:
    int Fibonacci(int n) {
        int a=0,b=1;
        while(n--){
            int c=a+b;
            a=b,b=c;
        }
        return a;
    }
};

22. 旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。

输入一个升序的数组的一个旋转,输出旋转数组的最小元素。

例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。

数组可能包含重复项。

注意:数组内所含元素非负,若数组大小为0,请返回-1。

样例

输入:nums=[2,2,2,0,1]

输出:0

class Solution {
public:
    int findMin(vector<int>& nums) {
        int n=nums.size()-1;
        if(n<0)return -1;
        while(n>=0&&nums[n]==nums[0])--n;//从数组末尾开始,去掉和nums[0]相等的
        if(nums[n]>nums[0])return nums[0];
        int l=0,r=n,ans;
        while(l<=r){//找小于nums[0]的第一个数
            int mid=(l+r)/1;
            if(nums[mid]<nums[0]){
                ans=nums[mid];
                r=mid-1;
            }else l=mid+1;
        }
        return ans;
    }
};

说明:二分,可画图观察旋转数组的单调性,找小于nums[0]的第一个数。

只有去掉数组末尾和nums[0]相等的元素(数组可能包含重复项)后才可进行二分(满足二分的条件)。

23. 矩阵中的路径

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。

路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。

如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子。

注意:

输入的路径不为空;

所有出现的字符均为大写英文字母;

样例

matrix=

[

  ["A","B","C","E"],

  ["S","F","C","S"],

  ["A","D","E","E"]

]

str="BCCE" , return "true"

str="ASAE" , return "false"

class Solution {
public:
    bool hasPath(vector<vector<char>>& matrix, string str) {
        for(int i=0;i<matrix.size();++i)
            for(int j=0;j<matrix[i].size();++j)
                if(dfs(matrix,str,0,i,j)) return true;//枚举起点
        return false;
    }
          
    bool dfs(vector<vector<char>>& matrix, string &str,int u,int x,int y){
        if(matrix[x][y]!=str[u])return false;//此句必须放下一句前面,否则str长度为1的测试点会WA
        if(u==str.size()-1)return true;
        int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
        char t=matrix[x][y];//为了回溯
        matrix[x][y]='*';//标记走过
        for(int i=0;i<4;++i){//枚举方向
            int a=x+dx[i],b=y+dy[i];
            if(a>=0&&a<matrix.size()&&b>=0&&b<matrix[a].size()){
                if(dfs(matrix,str,u+1,a,b)) return true;
            }
        }
        matrix[x][y]=t;//回溯
        return false;
    }
};

说明:暴力搜索。dfs里的参数:matrix和str是一直要带着跑的,u是步数,x,y是坐标。

 24. 机器人的运动范围

地上有一个 mm 行和 nn 列的方格,横纵坐标范围分别是 0∼m−10∼m−1 和 0∼n−10∼n−1。

一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格。

但是不能进入行坐标和列坐标的数位之和大于 kk 的格子。

请问该机器人能够达到多少个格子?

样例1

输入:k=7, m=4, n=5

输出:20

样例2

输入:k=18, m=40, n=40

输出:1484

解释:当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。

注意:

0<=m<=50

0<=n<=50

0<=k<=100

class Solution {
public:
    int get_sum(int n){
        int s=0;
        while(n) s+=n%10,n/=10;
        return s;
    }
    
    int movingCount(int threshold, int rows, int cols)
    {
        if(!rows||!cols) return 0;
        int X[4]={-1,1,0,0},Y[4]={0,0,-1,1};
        bool inq[55][55];
        memset(inq,false,sizeof(inq));
        int ans=1;
        queue<pair<int,int>>q;
        q.push({0,0});
        inq[0][0]=true;
        while(!q.empty()){
            auto top=q.front();
            q.pop();
            int a=top.first,b=top.second;
            for(int i=0;i<4;++i){
                int newa=a+X[i],newb=b+Y[i];
                if(newa>=0&&newa<rows&&newb>=0&&newb<cols&&get_sum(newa)+get_sum(newb)<=threshold&&!inq[newa][newb]){
                    ++ans;
                    q.push({newa,newb});
                    inq[newa][newb]=true;
                }
            }
        }
        return ans;
    }
};

说明:经典BFS。

25. 剪绳子

给你一根长度为 nn 绳子,请把绳子剪成 mm 段(mm、nn 都是整数,2≤n≤582≤n≤58 并且 m≥2m≥2)。

每段的绳子的长度记为k[0]、k[1]、……、k[m]。k[0]k[1] … k[m] 可能的最大乘积是多少?

例如当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到最大的乘积18。

样例

输入:8

输出:18

class Solution {
public:
    int maxProductAfterCutting(int n) {
        if(n<=3)return 1*(n-1);//题目的边界条件n>=2,m>=2
        int res=1;
        if(n%3==1) res*=4,n-=4;
        else if(n%3==2) res*=2,n-=2;
        while(n){
            n-=3;
            res*=3;
        }
        return res;
    }
};

说明:这里有个神奇的结论:要获得最大乘积,需要把N拆分成尽可能多的3+两个2/一个2。也即,如果N%3==1,拆分出两个2,剩下的全是3;如果N%3==2,拆分出一个2,剩下全是3;如果N%3==0,全是3。

证明:N>0, N=n1+n2+...+nk

1.假设 ni >= 5, 3 * (ni - 3) >= ni ? 3*ni - 9 >= ni ? 2*ni >= 9 ? √ 所以最优解里的所有数一定都小于5

2. ni = 4, 4 = 2 * 2 //所以4可以不在

3. 2 * 2 * 2 < 3 * 3 //所以不可能有3个2出现

26. 二进制中1的个数

输入一个32位整数,输出该数二进制表示中1的个数。

注意:

负数在计算机中用其绝对值的补码来表示。

样例1

输入:9

输出:2

解释:9的二进制表示是1001,一共有2个1。

样例2

输入:-2

输出:31

解释:-2在计算机里会被表示成11111111111111111111111111111110, 一共有31个1。

法1°:

class Solution {
public:
    int NumberOf1(int n) {
        return __builtin_popcount (n);
    }
};

说明:__builtin_popcount(n)返回n的二进制表示中1的个数。

法2°:

class Solution {
public:
    int NumberOf1(int _n) {
        unsigned int n=_n;
        int res=0;
        while(n) res+=n&1,n>>=1;
        return res;
    }
};

说明:先把有符号整数转化成无符号整数,这样就可以把一个负数转化成一个正数,转化的效果:n和_n的二进制表示是完全一样的,但值的含义发生了变化,这样一串二进制码,当它是有符号整数时它是一个负数,当它是无符号整数时它是一个很大的数。

补充:负数在计算机中用补码表示

补数的概念:

3对于10的补数是7,3和7互为补数

在二进制里,每个数的补数都是相对于二进制里非常整的一个数来说的,比如100000000000000000000000000000000

1 01 的补数 11111111111111111111111111111111

2 10 的补数 11111111111111111111111111111110

3 11 的补数 11111111111111111111111111111101

负数在计算机中用它绝对值的补码表示

-3 11111111111111111111111111111101 (3的补码)

27. 数值的整数次方

实现函数double Power(double base, int exponent),求base的 exponent次方。

不得使用库函数,同时不需要考虑大数问题。

注意:

不会出现底数和指数同为0的情况

样例1

输入:10 ,2

输出:100

样例2

输入:10 ,-2 

输出:0.01

class Solution {
public:
    double Power(double base, int exponent) {
        double res=1;
        for(int i=0;i<abs(exponent);++i) res*=base;
        if(exponent<0) res=1/res;
        return res;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值