牛客剑指offer笔记(上)

文章目录

点击查看《牛客剑指offer(下)》

https://www.nowcoder.com/ta/coding-interviews

在考虑算法时间复杂度小的情况下,还要尽量做到算法空间复杂度为O(1)! ! !

1.在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
这个思想还是要好好把握一下的!!!不要使劲暴力。

class Solution {
public:
    bool Find(int target, vector<vector<int> > array) {
        int row=array.size();
        int col=array[0].size();
        for(int i=row-1,j=0;i>=0&&j<col;){
            if(target>array[i][j]) j++;
            else if(target<array[i][j]) i--;
            else return true;
        }
        return false;
    }
};
2.请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

面试官可能也要要求空间和时间复杂度都小:
解题算法
关于string::resize()的用法
关于string::resize()的危险


class Solution {
public:
    string replaceSpace(string s) {
        int cnt=0;
        for(int i=0;i<s.size();i++){
            if(s[i]==' ') cnt+=2;
        }
        int idx=s.size()-1+cnt;//末尾索引
        int len=s.size();
        s.resize(idx+1);//扩充内存,并且该函数会保留下原数据
        //for(int i=0;i<cnt;i++) s.push_back('0');//也可以采用这个方法进行扩容
        for(int i=len-1;i>=0;i--){
            if(s[i]!=' ') s[idx--]=s[i];
            else{
                s[idx--]='0';
                s[idx--]='2';
                s[idx--]='%';
            }
        }
        return s;
    }
};
3.输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
据说翻转链表是面试中手撕代码环节高频题目。
下面这个方法比较好,时间和空间复杂度都低。
如果允许使用一点空间复杂度,那么可以定义两个数组,一个数组存储顺序遍历链表的结果,
另一个数组存储这个数组的逆序。但是这个方法面试官一般不喜欢。

class solution {
public:
    vector<int> printListFromTailToHead(ListNode* head) {
        ListNode* p=head,*q=NULL,*t;
        vector<int> v;
        while(p!=NULL){
            t=p->next;
            p->next=q;
            q=p;
            p=t;
        }
        head=q;
        while(q!=NULL){
            v.push_back(q->val);
            q=q->next;
        }
        return v;
    }
};
4.输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
就是递归!不过你还记得前序&中序和后序&中序怎么还原一颗二叉树吗?
前序&后序是不能构建二叉树的,因为他们的顺序分别是根左右、左右根,前序和后序
在本质上都是将父节点与子结点进行分离,但并没有指明左子树和右子树的能力,因此
得到这两个序列只能明确父子关系,而不能确定一个二叉树

class Solution {
public:
    TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
        if(vin.size()==0) return NULL;
        TreeNode* root=new TreeNode(pre[0]);//构建根节点
        vector<int> left_pre,right_pre,left_vin,right_vin;//当前树的左右子树前中序列
        int rootidx=-1;
        for(int i=0;i<vin.size();i++){//找到中序遍历中根节点位置
            if(pre[0]==vin[i]){
                rootidx=i;break; //说明左子树有rootidx个节点
            }
        }
        if(rootidx==-1) return NULL;
        for(int i=0;i<rootidx;i++){
            left_pre.push_back(pre[i+1]);//第i个对应中序中第rootidx个即根
            left_vin.push_back(vin[i]);
        }
        for(int i=rootidx+1;i<vin.size();i++){
            right_pre.push_back(pre[i]);//第i个对应中序中第rootidx个即根
            right_vin.push_back(vin[i]);
        }
        root->left=reConstructBinaryTree(left_pre,left_vin);//构建左子树
        root->right=reConstructBinaryTree(right_pre, right_vin);
        return root;
    }
    
};
5.用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
大体知道出栈的时候要把原栈数字压入到新的栈里再pop,但是忽略了pop之后要
再把新栈的数据压回原栈

class Solution
{
public:
    void push(int node) {
        stack1.push(node);
    }

    int pop() {
        while(!stack1.empty()){
            stack2.push(stack1.top());
            stack1.pop();
        }
        int num=stack2.top();
        stack2.pop();
        while(!stack2.empty()){
            stack1.push(stack2.top());
            stack2.pop();
        }
        return num;
    }

private:
    stack<int> stack1;
    stack<int> stack2;
};
6.把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。

输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

这是我二刷这些题目了,在这一遍刷题中,正确用更巧妙的算法,毕竟面试出这样的题
不是为了看你暴力表演的。
二分其实是很重要的考点,需要多多练习。

class Solution {
public:
    int minNumberInRotateArray(vector<int> rotateArray) {
        if(rotateArray.empty()) return 0;
        int left=0,right=rotateArray.size()-1;
        int mid=0;
        while(left<right){
            mid=(left+right)/2;
            if(rotateArray[mid]>rotateArray[right])  left=mid+1;
            else  if(rotateArray[mid]<rotateArray[right]) right=mid;
            else right--;
        }
         return rotateArray[left];
    }
};
7.大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。
第一遍刷这题的时候果然没有什么概念,,,直接就上递归了,但是要讲究效率,下面算法理解
很简单,复杂度为O(n)
class Solution {
public:
    int Fibonacci(int n) {
        if(n==0||n==1) return n;
        int tmp1=0,tmp2=1,val=0;
        for(int i=2;i<=n;i++){
            val=tmp1+tmp2;
            tmp1=tmp2;
            tmp2=val;
        }
        return val;
    }
};
8.一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
解题关键是:台阶有n阶,那么阶数为1的时候,只能跨一步;阶数为2的时候,可以跨两次一步,
或者两次一步。假设f(i)表示阶数为i时跳法个数。那么对于阶数i(2<i<=n),我们可以做如
下分析:假设最后一次是跳一步,那么f(i)=f(i-1);假设最后一次是跳两步,那么
f(i)=f(i-2).综上所述,f(i)=f(i-1)+f(i-2);
更多方法见:https://blog.csdn.net/weixin_38118016/article/details/79068755
这其实就是一个O(n)的斐波那契!

class Solution {
public:
    int jumpFloor(int number) {
        int *dp=new int [number+5];
        dp[1]=1;
        dp[2]=2;
        for(int i=3;i<=number;i++) dp[i]=dp[i-1]+dp[i-2];
        return dp[number];
    }
};


class Solution {
public:
    int jumpFloor(int number) {
        //f(n)=f(n-1)+f(n-2)
        if(number==1||number==2)return number;
        int tmp1=1,tmp2=2,val=0;
        for(int i=3;i<=number;i++){
            val=tmp1+tmp2;
            tmp1=tmp2;
            tmp2=val;
        }
        return val;
    }
};
9.一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

算法思想详解

//f(n)=f(n-1)+f(n-2)+f(n-3)+....+f(1)+f(0)
//f(n-1)=f(n-2)+f(n-3)+f(n-4)+...f(1)+f(0)
这道题和前一道题很相似,分析方法也可以同上:阶数为i的时候,假设最后一次是跳一步,
则f(i-1),假设最后一次是跳两步,则f(i-2),假设最后一次是跳三步,则f(i-3),,,假
设最后一次是跳i步,则f(0)。综上所述:f(i)=f(i-1)+f(i-2)+...+f(0),这显然不
好算,那么再列一个式子你就明白了,f(i-1)=f(i-2)+f(i-3)+...+f(0).两式相减:
f(i)=2(i-1)。

可以仿照上面进行O(n)算法求解,没必要递归啊,递归栈和时间复杂度不太讨喜
class Solution {
public:
    int jumpFloorII(int number) {
        if(number==1||number==2) return number;
        int tmp=2,val=0;
        for(int i=3;i<=number;i++){
            val=2*tmp;
            tmp=val;
        }
        return val;
    }
};



下面是递归解法,代码倒是挺简洁,但是面试的时候,真的不建议使用这个方法
class Solution {
public:
    int jumpFloorII(int number) {
        if(number==1) return 1;
        if(number==2) return 2;
        return 2*jumpFloorII(number-1);
    }
};
9.我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
这种递归如果实在想不出来怎么弄,可以找找规律(bushi
不过可以不用递归啊,仿照上面的斐波那契线性复杂度算法比递归更好
更一般的结论:如果用1*m的方块覆盖m*n区域,递推关系式为f(n) = f(n-1) + f(n-m)(n > m)

class Solution {
public:
    int rectCover(int number) {
        if(number<=0) return 0;
        else if(number==1) return 1;
        else if(number==2) return 2;
        else return rectCover(number-1)+rectCover(number-2);
    }
};
10.输入一个整数,输出该数32位二进制表示中1的个数。其中负数用补码表示。
相信很多小萌新拿到这个题想到的是暴力硬算,循环除2求余。遇到负数,再手动转化为补码存在一个数组里,,,
没错,这正是笔者以前的操作。
首先指出这个思路的一个大错误:为啥还要傻乎乎去自己写代码将数字转化为补码呢?要是忘记那些补码规则尤其是
边界值特殊值这种情况,反倒会把自己绕进去,,,别忘了!数字的机器语言本来就是补码啊!本来就是啊!不用手
动转化啊!
这里介绍一个我学到的一个非常高效率的算法(见代码):代码可能不太好理解,先看看我的例子,一个4位二进制
表达式1100,将其减11011,我们可以轻易得出规律:一串二进制减去1会使被减数从右往左第一个1开始到最右
边的数字取反,即
1110-1=1101
1110 0000-1=1101 1111。
将被减数和减法结果相与得:
1110&1101=11001110 0000&1101 1111=1100 0000
可见相与结果相比被减数就是少了一个1,所以,只要一直这样与减1的结果相与,便可统计出1的个数。一串二进制数
里面还有1。那么现在回到代码能看懂了吗?

Tips:面试的时候,你会立马用这个方法吗?最好不要,建议先用一个稍微笨一点的方法但是又不是
非常笨的方法。

class Solution {
public:
     int  NumberOf1(int n) {
        int count = 0;
        while(n!= 0){
            count++;
            n = n & (n - 1);
        }
        return count;
    }
};

另一种巧妙的方法,并且这一个方法更容易想到:
相信会有一些童鞋和我一样出同样的错误,就是想着通过右移n,来判断n中1的个数,但是!
运算符>>是算术右移,带符号的!所以对于负数右移高位会一直补1,从而死循环!
那么不妨用一个flag,通过左移flag去统计n中1的个数。

class Solution {
public:
     int  NumberOf1(int n) {
         int cnt=0;
         int flag=1;//0000...0001
         while(flag){
             if(n&flag) cnt++;
             flag=flag<<1;
         }
         return cnt;
     }
};

11.给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

保证base和exponent不同时为0

千万别使用powf函数,虽然结果没错,但是你跳过人家考点了啊!

class Solution {
public:
    double Power(double base, int exponent) {
        double res=base;
        for(int i=0;i<abs(exponent)-1;i++){
            res=base*res;
        }
        if(exponent==0) res=1;
        if(exponent<0) res=1.0/res;
        return res;
    }
};
12.输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
我觉得我这做法倒也没设计啥算法,就是单纯空间换时间,,,有时候倒也是个办法。
也可以用“插入排序”的思想,当然这里与排序毫无关系,就是遇到一个奇数就把它前面所有的偶数往后移一个,
把这个奇数插入到这堆偶数前面。

下面是二刷题目,空间复杂度O(1)

class Solution {
public:
    vector<int> reOrderArray(vector<int>& array) {
        if(array.empty()) return array;
        for(int i=1;i<array.size();i++){
            if(array[i]%2==1){//是奇数,则将前面的所有偶数后移一个单位
                int base=array[i],j=i-1;
                for(j=i-1;j>=0;j--){
                    if(array[j]%2==0) array[j+1]=array[j];//右移
                    else break;//要退出
                }
                j++;
                array[j]=base;
                cout<<"array["<<j<<"]"<<array[j]<<"\n";
            }
        }
        return array;
    }
};
13.输入一个链表,输出该链表中倒数第k个结点。
双指针,一个指针先移动k个节点,然后两个指针一起移动

class Solution {
public:
    ListNode* FindKthToTail(ListNode* pHead, int k) {
        ListNode *p1=pHead,*p2=pHead;
        for(int i=0;i<k;i++){
            if(p1==NULL) return NULL;
            p1=p1->next;
        }
        while(p1!=NULL){
            p1=p1->next;
            p2=p2->next;
        }
        return p2;
        // write code here
    }
};
14.反转链表
class Solution {
public:
    ListNode* ReverseList(ListNode* pHead) {
        ListNode* p=pHead,*q=NULL,*t=NULL;
        while(p!=NULL){
            t=p->next;
            p->next=q;
            q=p;
            p=t;
        }
        pHead=q;
        return pHead;
    }
};
15.输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
细节错误,你确定你新建的节点真的链接到链表后面了吗?
对于这个老生常谈的错误,还是看看这篇文章吧:https://blog.csdn.net/qq_44090228/article/details/114682770

好家伙,二刷题目时候,竟然没有去更新mergenode,后面的表都没链上去
好家伙!

class Solution {
public:
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
        if(pHead1==NULL) return pHead2;
        if(pHead2==NULL) return pHead1;
        ListNode* mergenode=NULL,*p1=pHead1,*p2=pHead2;
        ListNode* mergehead=NULL;
        if(p1->val<p2->val) {//确定头节点
            mergehead=p1;
            p1=p1->next;
        }
        else{
            mergehead=p2;
            p2=p2->next;
        }
        mergenode=mergehead;
        while(p1!=NULL&&p2!=NULL){
            if(p1->val<p2->val){
                mergenode->next=p1;
                mergenode=p1;//二刷的时候,这一句更新mergenode竟然没加上去,希望能将这个错误刻骨铭心
                p1=p1->next;
            }
            else{
                mergenode->next=p2;
                mergenode=p2;
                p2=p2->next;
            }
        }
        
        while(p1!=NULL){
            mergenode->next=p1;
            mergenode=p1;
            p1=p1->next;
        }
        while(p2!=NULL){
            mergenode->next=p2;
            mergenode=p2;
            p2=p2->next;
        }
        return mergehead;
    }
};
17.输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
一开始尝试使用层次查找,找到值相同的点后,再从这个点开始进行层次比较,思路肯定很粗暴,
关键是代码也难写难懂还出错
采用递归的思想,仔细阅读下面代码即可弄懂。

class Solution {
public:
    bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2) {
        bool res=false;
        if(pRoot2==NULL||pRoot2==NULL) return false;
        if(pRoot1!=NULL&&pRoot2!=NULL){//感觉这句不用写,,,但不写就报段错误
            if(pRoot1->val==pRoot2->val)  
                res=cmp(pRoot1,pRoot2);//从这里开始比较
            if(!res) res=HasSubtree(pRoot1->left,pRoot2);//如果上面的不匹配,再对比A树中以下一个节点为根的子树与B树是否匹配
            if(!res) res=HasSubtree(pRoot1->right,pRoot2);
        }
        return res;
    }
    bool cmp(TreeNode* t1, TreeNode* t2){//开始比较两棵树,注意要对A树有效截取,只比较B树有的地方。
        if(t2==NULL) return true;
        if(t1==NULL) return false;
         if(t1->val!=t2->val) return false;
        return cmp(t1->left,t2->left)&&cmp(t1->right,t2->right);
        }
};
18.操作给定的二叉树,将其变换为源二叉树的镜像。
层次遍历做法  递归做法

class Solution {
public:
    TreeNode* Mirror(TreeNode* pRoot) {
        if(pRoot==NULL) return pRoot;
        queue<TreeNode*> q;
        q.push(pRoot);
        while(!q.empty()){
            TreeNode*u=q.front();
            q.pop();
            if(u->left==NULL&&u->right==NULL) continue;
            TreeNode* t=u->left;//开始交换
            u->left=u->right;
            u->right=t;
            if(u->left!=NULL) q.push(u->left);
            if(u->right!=NULL) q.push(u->right);
        }
        return pRoot;
    }
};

递归做法:
    TreeNode* Mirror(TreeNode* pRoot) {
        if(pRoot==NULL) return pRoot;
        if(pRoot->left==NULL&&pRoot->right==NULL) return pRoot;
        TreeNode*t=pRoot->left;
        pRoot->left=pRoot->right;
        pRoot->right=t;
        if(pRoot->left) Mirror(pRoot->left);
        if(pRoot->right) Mirror(pRoot->right);
        return pRoot;
    }
19.输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
太卷了,细节决定成败,思路很简单,就直接暴力就行,但是写代码时老是多次出现重复
输出相同的值了,这主要是当前遍历的矩阵圈内,没有判断第一行和最后一行的行号是否
一样或者第一列和最后一列列数是否相同,直接就一个for循环遍历,从而重复遍历了,
大问题不多,小问题一箩筐.....

class Solution {
public:
    vector<int> printMatrix(vector<vector<int> > matrix) {
        vector<int> v;
        int row=matrix.size(),col=matrix[0].size();
        int ibegin=0,iend=row-1,jbegin=0,jend=col-1;
        int i,j,cnt=0;
        int flag=row*col;
        while(cnt<flag){
	        for(int j=jbegin;j<=jend;j++) {
                v.push_back(matrix[ibegin][j]);
                cnt++;        
             }
            for(int i=ibegin+1;i<=iend;i++) {
                v.push_back(matrix[i][jend]);
                cnt++;
            }
            if(iend!=ibegin){
                for(int j=jend-1;j>=jbegin;j--) {
                    v.push_back(matrix[iend][j]);
                    cnt++;
                }
            }
            if(jbegin!=jend){
                for(int i=iend-1;i>=ibegin+1;i--) {
                    v.push_back(matrix[i][jbegin]);
                    cnt++;
                }
            }
            ibegin++;iend--;
            jbegin++;jend--;
        }
        return v;
    }
};
20.定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。
定义两个栈,一个栈保存所有元素,一个栈保存小在上,大在下的规则存储数。
原来的思路是用一个栈保存,并且是小在上,大在下的规则存储,也就是说压栈的时候,只要栈内
元素比value值小,就把栈内元素弹出,压入value后,在压回弹出的元素。这思路肯定错了,这
样做栈内的元素顺序根本不是题意要求的,题意没要求栈内排序
后来又企图记录最小值minval,然后返回该值,明显还是误解了题意,万一执行pop操作后,最小
数已经被删除了,你的minval还是原来的最小值,显然不合理。
正确做法如下:

class Solution {
public:
    stack<int> st1,st2;
    void push(int value) {
        if(st2.empty()||st2.top()>value) st2.push(value);
        st1.push(value);
    }
    void pop() {
        if(st1.top()==st2.top()){
            st1.pop();
            st2.pop();
        }
        else st1.pop();
    }
    int top() {
        return st1.top();
    }
    int min() {
        return st2.top();
    }
};
21.输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)
太傻了,应该没有人和我一样那么单纯地以为pushV和倒序的popV一样才true,TAT
这道题要借助辅助栈的思路!

class Solution {
public:
    bool IsPopOrder(vector<int> pushV,vector<int> popV) {
        int lenpush=pushV.size(),lenpop=popV.size();
        stack<int> s;
        int j=0;
        for(int i=0;i<lenpush;i++){
            s.push(pushV[i]);
            while(!s.empty()&&s.top()==popV[j]){
                s.pop();
                j++;
            }
        }
        if(s.empty()) return true;
        else return false;
    }
};
22.从上往下打印出二叉树的每个节点,同层节点从左至右打印。
是一个简单的层次遍历问题,但是这里有一个点很BT,就是要判断root非NULL的时候才入栈,
那我就纳闷了,,,root为空,返回值v是个啥??我这里又没有初始化v,,

class Solution {
public:
    vector<int> PrintFromTopToBottom(TreeNode* root) {
        queue<TreeNode*> q;
        vector<int> v;
        if(root!=NULL) q.push(root);
        TreeNode* tmp;
        while(!q.empty()){
            tmp=q.front();
            q.pop();
            v.push_back(tmp->val);
            if(tmp->left!=NULL) q.push(tmp->left);
            if(tmp->right!=NULL) q.push(tmp->right);
        }
        return v;
    }
};

23.输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则返回true,否则返回false。假设输入的数组的任意两个数字都互不相同。(ps:我们约定空树不是二叉搜素树)

序列的最后一个节点是根节点,那么这个序列左边一部分序列是左子树,右边一部分是右子树,
所以只需要找到第一个比根节点大的,就可视为从这里开始后面是右子树,前面是左子树,那
么一旦在右子树中出现一个比根节点小的值,这就一定不是一个搜索树的后序序列。
记录下左右子树节点在数组中的起止索引,之后又对左右子树分别递归判断是否符合搜索树后
序序列。

class Solution {
public:
    bool VerifySquenceOfBST(vector<int> sequence) {
        int start=0,end=sequence.size()-1;
        if(sequence.size()<=0) return false;
        return if_same(sequence,start,end);
    }
    
    bool if_same(vector<int> sequence,int start,int end){
        if(end<=start) return true;//达到递归边界
        int i=start;//从这里开始时左子树
        while(sequence[i]<sequence[end]) i++;
        int j=i;//从这里开始时右子树
        for(j=i;j<end;j++){
            if(sequence[j]<sequence[end]) return false;
        }
        return if_same(sequence,start,i-1)&&if_same(sequence,i,end-1);
        //分别递归判断左右子树序列是否是搜索树后序
    }
};
24.输入一颗二叉树的根节点和一个整数,按字典序打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
理解好题意,这里的路径一定是从根节点到叶节点的路径(就离谱!
一开始做的时候,以为是从根节点开始到达任意节点的路径满足和为expectnumber就行,这里挺容易歧义的。
如果不太理解这道题,就去查阅《剑指offer》上的指导。

class Solution {
public:
    //这类问题可以用带记忆的DFS来解决。分享一个这类问题的典型解法。
    vector<vector<int> > FindPath(TreeNode* root,int expectNumber) {
        vector<vector<int>> ret;//所有路径集
        vector<int> trace;//单条路径
        if(root)
            dfs(root,expectNumber,ret,trace);
        return ret;
    }
    void dfs(TreeNode* root,int s,vector<vector<int>> &ret,vector<int> &trace) {
        trace.push_back(root->val);
        if(!root->left&&!root->right) {//这里就是保证已经到了叶节点
            if(s==root->val)//判断expectNumber-sum(root->val)==0
                ret.push_back(trace);
        }
        if(root->left)
            dfs(root->left,s-root->val,ret,trace);
        if(root->right)
            dfs(root->right,s-root->val,ret,trace);
        trace.pop_back();
        //如果当前递归的参数是一个叶子且目前路径和不为expectnumber
        //但是又因为dfs函数第一行已经把这个叶子节点值存入数组了,所以要把这个叶子节点pop出
    }
};

25.复杂链表的复制,输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。

(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)。 下图是一个含有5个结点的复杂链表。图中实线箭头表示next指针,虚线箭头表示random指针。为简单起见,指向null的指针没有画出。

class Solution {
public:
    RandomListNode* Clone(RandomListNode* pHead) {
        if(pHead==NULL) return NULL;
        RandomListNode* cur=pHead;
        map<RandomListNode*,RandomListNode*> mp;
        while(cur!=NULL){
            mp[cur]=new RandomListNode(cur->label);
            cur=cur->next;
        }
        cur=pHead;
        while(cur!=NULL){
            mp[cur]->next=mp[cur->next];
            mp[cur]->random=mp[cur->random];
            cur=cur->next;
        }
        return mp[pHead];
    }
};

//方法2:目前这个方法老是提示我是在返回原链表,,但我明明不是,,,尝试过手动输出结果,问题也不大,,,
class Solution {
public:
    RandomListNode* Clone(RandomListNode* pHead) {
        if(pHead==NULL) return NULL;
        RandomListNode* cur=pHead;
        while(cur!=NULL){
            RandomListNode* node=new RandomListNode(cur->label);
            node->next=cur->next;
            cur->next=node;
            cur=cur->next->next;
        }
        cur=pHead;
        while(cur!=NULL&&cur->next!=NULL){
            if(cur->random!=NULL) cur->next->random=cur->random->next;
            cur=cur->next->next;
        }
        RandomListNode* pre=pHead,*res=pHead->next;
        cur=res;
        while(cur!=NULL&&cur->next!=NULL){
            pre=pre->next->next;//前驱指针移动到下一个,这里的下一个是原链表中的下一个
            cur->next=pre->next;
            cur=cur->next;
        }
        return res;
    }
};
26.输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

算法详解

这个题如果允许建立新的节点就很简单,可以采取中序遍历,然后从头到尾建立链接关系即可。
但是题中要求只能调整树中的节点指向,所以就又要递归。但是递归向来是我的弱点,,,
刷题之路还要继续,,,

class Solution {
public:
    TreeNode* Convert(TreeNode* pRootOfTree) {
        if(pRootOfTree==NULL) return NULL;
        TreeNode* last=NULL;
        //假设是双链表的最后一个节点,root的left指向last,last的right指向root
        transfer(pRootOfTree,last);
        while(pRootOfTree->left!=NULL) pRootOfTree=pRootOfTree->left;//取得双向链表的头指针
        return pRootOfTree;
    }
    void transfer(TreeNode*root,TreeNode* &last){//引用指针,那么就可以改变实参~
        if(root==NULL) return;
        transfer(root->left,last);//遍历左子树
        root->left=last;
        if(last!=NULL) last->right=root;
        last=root;//左子树构造结束,last->..->..->root,所以要将last指向root
        //last指向root后,又同理开始构造右边的序列。
        transfer(root->right,last);//遍历右子树
    }
};
27.输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则按字典序打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。/font>
全排列!!全排列!《数据结构算法与应用》第一章就讲的东西
不过我至今都理解不透,目前处于背代码阶段,,,
但是注意,这里存在重复的字符,所以最后的结果要去重。
另外,我觉得还有一张思路:把字符串存储成完全图,然后dfs,转化成多少条路径,,,

class Solution {
public:
    vector<string> Permutation(string str) {
        int k=0,m=str.size()-1;//首位索引
        vector<string> res,res2;
        merge(res,str,k,m);
        sort(res.begin(),res.end());
        res2.push_back(res[0]);
        int idx=0;
        for(int i=1;i<res.size();i++){
            if(res[i]!=res2[idx]){
                res2.push_back(res[i]);
                idx++;
            }
        }
        return res2;
    }
    void merge(vector<string>& res,string str,int k,int m){
        if(k==m){//递归边界,到这里,那么字符串的一个排列就完成了
            res.push_back(str);
        }
        else{
            for(int i=k;i<=m;i++){
                swap(str[i],str[k]);
                merge(res,str,k+1,m);
                swap(str[i],str[k]);
            }
        }
    }
};
28.数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

空间复杂度O(1)时间复杂度O(n)的两种算法

采用哈希的思想~
但是要仔细阅读题意,题目中要求出现次数是超过一半,是超过!不能是等于一半。
比如数组有7个数字,那么如果出现4次就是符合题意的,7/2+1
比如数组有8个数字,那么如果出现5次才是符合题意的,8/2+1
即出现次数count>=len/2+1
采用哈希思想的代码很简单,用map或者单独开一个次数记录的数组,这里不再展示代码,下面主要学习另外两种方法
既可以高效又可以空间复杂度O(1)

方法1:找出出现次数最多的数字,再统计这个数字出现的次数,如果没有超过长度一半,那么其他也不可能,如果超
出一半那就返回这个数。
class Solution {
public:
    int MoreThanHalfNum_Solution(vector<int> numbers) {
        if(numbers.empty()) return 0;
        int res=0,cnt=0;
        res=numbers[0],cnt=1;
        for(int i=0;i<numbers.size();i++){//找出出现次数最多的数字
            if(cnt==0) {//统计新的数字
                res=numbers[i];
                cnt=1;
            }
            else if(res==numbers[i]) cnt++;//如果下一个数字和res相同,那么cnt++
            else cnt--;//如果cnt!=0并且这个数字不与res相等,则cnt--
        }
        //再判断该数的出现的次数是否超过一半
        cnt=0;
        for(int i=0;i<numbers.size();i++){
            if(res==numbers[i]) cnt++;
        }
        if(cnt>numbers.size()/2) return res;
        return 0;
    }
};

方法二:基于快排,局部排序,统计上的中位数定义如果存在满足条件的数,则肯定该数为
数组的中位数.在数组中随机选择一个数,然后调整数组中数字的顺序,使得比选中的数字
小的数字都排在它的左边,比选中的数字大的数字都排在它的右边。如果这个选中的数字的
下标刚好是n/2,那么这个数字就是数组的中位数;如果它的下标大于n/2,那么中位数应该
位与它的左边,我们可以接着在它的左边部分数组中查找,如果它的下标小于n/2,那么中
位数位与它的右边,我们可以接着在它的右边部分数组中查找。

class Solution {
public:
    int MoreThanHalfNum_Solution(vector<int> numbers) {
        if(numbers.empty()) return 0;
        int len=numbers.size();
        int mid=len/2,start=0,end=len-1;
        int idx=quicksort(numbers,start,end);
        while(idx!=mid){
            if(idx>mid){
                end=idx-1;
                idx=quicksort(numbers,start,end);
            }
            else{
                start=idx+1;
                idx=quicksort(numbers,start, end);
            }
        }
        int res=numbers[mid];//中位数一定在mid位置
        int cnt=0;
        for(int i=0;i<numbers.size();i++){
            if(numbers[i]==res) cnt++;
        }
        if(cnt*2>len) return res;
        return 0;

    }
    int quicksort(vector<int>& num,int left,int right){
        int base=num[left];
        while(left<right){
            while(left<right&&num[right]>=base) right--;
            num[left]=num[right];
            while(left<right&&num[left]<=base) left++;
            num[right]=num[left];
            
        }
        num[left]=base;
        return left;
    }

};

29.给定一个数组,找出其中最小的K个数。例如数组元素是4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。如果K>数组的长度,那么返回一个空的数组
基于快排,基于数组第k个数字的来调整,则使得比第k个数字小的所有数字都位与数组的左边,
比第k个数字大的所有数字都位与数组的右边,按照这样调整之后,位与数组中左边的k个数字
就是最小的k个数字,但是这个k个数字不一定是有序的。
但是这个算法也需要提前把所有待处理的数据全部预读入。那么要是去处理很大的数据量,这个
算法还能用吗?
class Solution {
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        vector<int> res;
        if(input.empty()||k>input.size()||k<=0) return res;
        int left=0,right=input.size()-1,idx=0;
        idx=quicksort(input,left,right);
        while(idx!=k-1){
            if(idx>k-1){
                right=idx-1;
                idx=quicksort(input,left,right);
            }
            else{
                left=idx+1;
                idx=quicksort(input,left,right);
            }
        }
        for(int i=0;i<k;i++) res.push_back(input[i]);
        return res;
    }
    int quicksort(vector<int>& v,int left,int right){
        int base=v[left];//基值
        while(left<right){
            while(left<right&&v[right]>=base) right--;
            v[left]=v[right];
            while(left<right&&v[left]<=base) left++;
            v[right]=v[left];
        }
        v[left]=base;
        return left;
    }
};

方法二:堆排序,维持一个节点数为k的最大堆。可以用来处理海量数据。维护一个只有k个节点的大根堆,只要
数组中下一个元素小于根,那么就删除根,并将这个数插入到堆中。
class Solution {
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        vector<int> res;
        if(input.empty()||k<=0||k>input.size()) return res;
        priority_queue<int> q;//最小堆
        for(int i=0;i<k;i++){//k个数先入队
            q.push(input[i]);//相反数入队,即最大堆
        }
        for(int i=k;i<input.size();i++){
            int t=q.top();
            if(input[i]<t) {
                q.pop();
                q.push(input[i]);
            }
        }
        while(!q.empty()) {
            res.push_back(q.top());
            q.pop();
        }
        return res;
    }
};

30.输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为 O(n).
暴力求解:枚举出子数组的所有长度,再枚举出子数组的起始点,时间复杂度O(n^2)
class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        int res=0x80000001;
        for(int i=1;i<=array.size();i++){//子数组长度
            int sum=0,cnt=0,begin=0;
           for(int j=begin;j<array.size();){//从这里开始
               if(cnt==i){
                   res=max(res,sum);
                   cnt=0;sum=0;
                   begin++;j=begin;
               }
               sum+=array[j];j++;cnt++;
           }
            if(cnt==i) res=max(res,sum);
        }
        return res;
    }
};

//方法2:小白做法
这题可以用两层循环求解,第一层我们遍历整个数组,确定现在我们找的连续数组的边界在
哪里,第二层我们遍历边界前的所有数,进行相加,一旦出现数字比我们当前拥有的max大的,我们就
记录下来。这样我们可以确保最后的max是整个数组的连续子数组最大和。
class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        if(array.empty()) return 0;
        int max=array[0],j=0;
        for(int i=0;i<array.size();i++){
            if(max<array[i])   max=array[i];
            int j=i-1,tmp=array[i];
            while(j>=0){
                tmp+=array[j];
                if(tmp>max) max=tmp;
                j--;
            }
        }
        return max;
    }
};

方法3:时间复杂度O(n),初始化和为0,第一步加上第一个数字1,此时和为1,第二步
加上数字-2, 和变成了-1,第三步加上数字3,那么结果会变成2,比本身还小!也就是说,
从第一个数字开始的子数组的和会小于从第三个数字开始的子数组的和,因此前面的累加可
以抛弃,但是要把每次的累加和保存起来。这里要注意:0xffffffff-1的补码,0x80000001-2147483647的补码
有时候记不住最小边界值的时候可以尝试使用这种形式。

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        int max=0x80000001,tmp_max=0x80000001;//初始化为最小边界值。
        for(int i=0;i<array.size();i++){
            if(tmp_max<=0) tmp_max=array[i];//重新定义当前和
            else tmp_max+=array[i];
            if(max<tmp_max) max=tmp_max;
        }
        return max;
    }
};


class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        if(array.empty()) return 0;
        int max=0x80000001,tmp=0;
        for(int i=0;i<array.size();i++){
            tmp+=array[i];//先加上这个数字
            if(tmp<array[i]) tmp=array[i];
            //发现加上这个数字后,反倒比加数小了,说明我们可以抛弃前面的累加和,选择这个加数就可以
            
            if(tmp>max) max=tmp;//累加和大于当前max,更新max
        }
        return max;
    }
};
31.求出1-13的整数中1出现的次数,并算出100-1300的整数中1出现的次数?

为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。

//字符串解法:
class Solution {
public:
    int NumberOf1Between1AndN_Solution(int n) {
        string str="";
        for(int i=1;i<=n;i++){
        //把每个数字转化为字符串,总和到str中,之后统计字符1的个数
            stringstream ss;
            ss<<i;
            string s;
            ss>>s;
            str+=s;
        }
        int cnt=0;
        for(int i=0;i<str.size();i++) {
            if(str[i]-'0'==1)
            cnt++;
        }
        return cnt;
    }
};

//找规律解法:一定要掌握!
https://blog.csdn.net/yi_afly/article/details/52012593  一定要去查看这篇文章!!! 
class Solution {
public:
    int NumberOf1Between1AndN_Solution(int n) {
        int weight=n,cnt=0,round=n,base=1;
        while(round>0){
            weight=round%10;
            round/=10;
            if(weight==1) cnt+=n%base+1;
            if(weight>1) cnt+=base;
            cnt+=round*base;
            base*=10;
        }
        return cnt;
    }
};
32.输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
主要就是排序,这里我使用了冒泡排序,具体比较两个字符串s1,s2时,只需计算出
s1+s2和s2+s1的大小关系,如果s1+s2更小,说明s1应该在s2前面,否则s2就在s1
前面。
class Solution {
public:
    string PrintMinNumber(vector<int> numbers) {
        vector<string> v;
        string res="";
        if(numbers.empty()) return res;
        for(int i=0;i<numbers.size();i++){//转化为字符串
            stringstream ss;
            ss<<numbers[i];
            string tmp;
            ss>>tmp;
            v.push_back(tmp);
        }
        for(int i=0;i<v.size()-1;i++){//冒泡排序
            for(int j=0;j<v.size()-i-1;j++)
                bubblesort(v[j],v[j+1]);
        }
        for(int i=0;i<v.size();i++)  res+=v[i];
        return res;
        
    }
    void bubblesort(string &s1,string &s2){
        string s1s2="",s2s1="";
        s1s2+=s1+s2;
        s2s1+=s2+s1;
        if(s1s2>s2s1) swap(s1,s2);
    }
};
33.把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
先理解什么是丑数:丑数是另一个丑数乘以2,35的结果(1除外)。

思路一:理解起来很有难度。
查看算法详情:https://blog.csdn.net/Fly_as_tadpole/article/details/82705774
大概就是维持三个队列指针,这个指针分别指向x2,x3,x5的元素位置,,,

class Solution {
public:
    int GetUglyNumber_Solution(int index) {
        if(index<=1) return index;
        vector<int> v;//分别记录当前丑数乘以2,3,5的值
        int newnum=1,p2=0,p3=0,p5=0;//选中的队列元素,指向2,3,5队列到达的位置
        int cnt=1;
        v.push_back(newnum);
        while(cnt<index){
            newnum=min(v[p2]*2,min(v[p3]*3,v[p5]*5));
            if(v[p2]*2==newnum) p2++; 
            if(v[p3]*3==newnum) p3++;
            if(v[p5]*5==newnum) p5++;
            v.push_back(newnum);
            cnt++;
        }
        return newnum;
    }
};

使用一个队列的话,就得使用最小优先队列,每次优先处理最小的,并且还要有一个vis数组
进行标记,防止重复入队。但是问题是,这个vis数组定义多大的内存空间呢,如果题干中要
求的丑数是一个很大的值,vis数组会不会内存溢出。所以这个方法做很不好处理,会出现段
错误。
用map会解决段错误,但是测试数据太大的话,就会导致WA,该题过12/13组测试点。  
对了! map有容量限制 max_size=268435455,会不会是这个原因呢?

class Solution {
public:
    int GetUglyNumber_Solution(int index) {
        if(index<7) return index;
        priority_queue<int> pq;
        int flag[]={2,3,5};
        pq.push(-1);//相反数入大根堆,相当于小根堆
        int cnt=0,num=1;
        unordered_map<int,int> ump;
        while(cnt<index&&!pq.empty()){
            num=pq.top(); pq.pop();
            cnt++;
            for(int i=0;i<3;i++){
                if(ump[0-num*flag[i]]==0){
                    pq.push(num*flag[i]);
                    ump[0-num*flag[i]]=1;
                }
            }
        }
        return 0-num;
    }
};



使用set,可以pass
if(index<=6) return index;
	set<int> se;
	for(int i=1;i<=6;i++) se.insert(i);
	int cnt=0;
    auto it=se.begin();
	while(!se.empty()){
		int base=*it;//最小丑数
		cnt++;
        if(cnt==index) return *it;
		se.insert(base*2);
		se.insert(base*3);
		se.insert(base*5);
        it++;
	}
	return 0;


34.在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).(从0开始计数)
采用哈希的思路可以高效计算出来,另外题目中已经告知了字符串长度,可以不需要去使用stl中的map,建议
简单申明一个常数大小的数组,这样一来,空间复杂度也就O(1)

class Solution {
public:
    int FirstNotRepeatingChar(string str) {
        map<char,int> mp;
        for(int i=0;i<str.size();i++) mp[str[i]]++;
        for(int i=0;i<str.size();i++){
            if(mp[str[i]]==1) return i;
        }
        return -1;
    }
};

JZ34 第一个只出现一次的字符
使用bitset更加节省空间,只聚焦处理前两次出现,第一次出现b1[ch]=1,
第二次出现b2[ch]=1,没有出现的话,就等于0class Solution {
public:
    int FirstNotRepeatingChar(string str) {
        bitset<128> b1,b2;//分别表示第一次出现、第二次出现,思路:只聚焦于前两次出现
        for(int i=0;i<str.size();i++){
            if(!b1[str[i]]&&!b2[str[i]]) b1[str[i]]=1;
            else if(b1[str[i]]&&!b2[str[i]]) b2[str[i]]=1;
        }
        for(int i=0;i<str.size();i++)
            if(b1[str[i]]&&!b2[str[i]]) return i;
        return -1;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值