剑指offer题集

因为面试前需要快速过一遍剑指offer,共68道,题目太多非常耗费时间,在此只做一遍值得一做的题目。凭我个人的能力(约100+道leetcode水平)是可以完全忽略一些题目的。这里仅给出python代码及思路

1.两个栈实现一个队列。

此题不难,但是需要考虑减少无用操作,时刻记住第二个栈已经是倒序的了,所以只要没pop完就不要再往里append。

class CQueue:

    def __init__(self):
        self.stk1=[]
        self.stk2=[]



    def appendTail(self, value: int) -> None:
        self.stk1.append(value)



    def deleteHead(self) -> int:
        if len(self.stk1)+len(self.stk2)==0:
            return -1
        #注意,stk2是原有stk1的倒序,所以只要stk2里面还有元素就可以直接往外pop
        if self.stk2:
            return self.stk2.pop()
        else:
            while self.stk1:
                self.stk2.append(self.stk1.pop())
            return self.stk2.pop()

2.斐波那契数列第N个数字。

这里有个溢出的问题(当然python是没有的)一旦有着中需要结果取模的,其实只要在运算中取模就好了。这里给出c++代码。

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

3.数组中的重复数字

这里最优秀的办法是将数组当做哈希表用,从题目可以看出会有多个数字对应一个索引,那么只要遇到一个不在索引位置上的数字,就将其swap到索引位置,只要发现已经被交换过了,说明已经被放置过一个对应索引数字,就返回就行了。

class Solution:
    def findRepeatNumber(self, nums: List[int]) -> int:
        i=0
        while i<len(nums):
            
            if i==nums[i]:#已经在对应位置了,不需要管,假如说这个数字无重复,那无所谓;假如有重复,那么下一次swap就会判断出啦
                i+=1
                continue
            idx=nums[i]
            if idx==nums[idx]:#这个表示即将swap的位置已经被对应idx占领了,直接返回
                return idx
            nums[idx],nums[i]=nums[i],nums[idx]
        return -1

4.旋转数组中位数

这题只要说有重复数字了,那可是相当不简单

class Solution:
    def minArray(self, numbers: List[int]) -> int:
        lo,hi=0,len(numbers)-1
        while lo<=hi:
            mid=lo+(hi-lo)//2
            #如果中间的比最右边的小比如3 4 1 2 3 3那不用犹豫,最小值一定在左半边
            if numbers[mid]<numbers[hi]:
                hi=mid
            #如果中间的比最右边的还大,那一定在右半边
            elif numbers[mid]>numbers[hi]:
                lo=mid+1
            #如果中间的和最右边一样大,尝试向左缩减范围直到number[lo]变小再比较,就成了情况2
            else:
                hi-=1
        return numbers[lo]

5.剪绳子

其实是一道不难的dp题(有人做成数学题了),我觉得用dp足够了。

假设dp[i]是长度为i的绳子能达到的最大乘积,那么如果在j的位置剪了一次,那么就是j*(j-i),如果是大于1次的,就是j*dp[j-i],也就是其中一段是j,乘以剩下能剪出的最大值。

class Solution:
    def cuttingRope(self, n: int) -> int:
        dp=[0]*(n+1)
        for i in range(2,n+1):
            for j in range(1,i):
                dp[i]=max(j*(i-j),j*dp[i-j],dp[i])
        return dp[-1]

6.二进制中1个数

这个题除了逐位计算,巧妙一点的是抵消法

考虑n=1001 ,n-1=1000,二者相&,的得到n=1000

迭代n-1=0111,n&=n-1等于0就可以终止了

事实上这个方法能够快速的抹去其中的一串0,但是不会放过任何一个最后一个1.

class Solution {
public:
    int hammingWeight(uint32_t n) {
        int cnt=0;
        while(n){
            n&=n-1;
            cnt++;
        }
        return cnt;
    }
};

7.x的n次幂

对于这题总想用递归算,其实不用,类似动态规划一样即可

我们知道2^5=2^(0b101)=2^1*2^4,这样

而我们知道

x=x^1

x*=x == x^2

x*=x == x^4

这不就有了你看

class Solution:
    def myPow(self, x: float, n: int) -> float:
        if x==0.0:
            return x
        if n<0:
            x,n=1.0/x,-n
        res=1.0
        while n:
            if n%2==1:
                res*=x
            x*=x
            n>>=1
        return res

8.正则匹配

9.1~n 整数中 1 出现的次数

此题为一道找规律的题,但似乎有更牛逼的数学算法,但是这里还是用找规律的算法,这样面试时做出来也不太尴尬。

考虑一个两位数,比如36,他的十位和个位分别能产生多少个1呢?答案是14,但是怎么来的?

14=10**1 + 4,只要十分位超过了1,那么在10,11,12....19这个序列中就有了10个(仅考虑十分位),而在十分位0,1,2,3循环后,个位出现了4次1。

我们再考虑一个更复杂的,365325.

按照思路第一位(第一个3)经历前一位0-1循环,第二位经历0-36循环,第三位经历0-365循环。

经历循环后又怎么计算呢?其实就很容易得出整数的计算法,j就像我们前面考虑10,11...19的时候,就知道其实每位能产生的1个数,是和当前位置有关系的。也就是

1*10**5+4*10**4+36*10**3+366*10**2+3654*10**1+36533

但是考虑两个特殊情况,如果这一位是0的话(当然不可能出现在第一位),比如32033,那么实际上0这一位没有构成1,所以这个时候应该以32*10**2方式计算这一位的1而不是33*10**2;

如果这一位恰好为1的时候,比如32133,那么1这一位并没有由于经历这一位从1变为2的过程,所以后面不应该丢掉取整数,相当于10,11,12,13我们应该只计算成4个而不是10个。这样的话相当于32*10**2+33,或者更粗暴一点的描述就是“去掉这一位构造出1的个数。

这样求和就很容计算了。

class Solution:
    def countDigitOne(self, n: int) -> int:
        if n<10:
            return 1
        nums=[int(c) for c in str(n)]
        ns=str(n)
        ret=0
        for i in range(len(nums)):
            if i==0:
                if nums[i]==1:
                    ret+=int(ns[1:])+1
                else:
                    ret+=10**(len(nums)-1)
            else:
                if nums[i]==0:
                    ret+=int(ns[:i])*10**(len(nums)-i-1)
                elif nums[i]==1:
                    ret+=int(ns[:i]+ns[i+1:])+1
                else:
                    ret+=(int(ns[:i])+1)*10**(len(nums)-i-1)
        return ret

10.判断是否为后序遍历

此题很容易发现只要递归判断即可。因为序列都是左-右-根模式,扩展一下,那么序列一定是[左子树节点集合,右子树节点集合,根],我们每次把根取出来然后递归判断两部分即可。

由于BST的性质,左子树应该是小于根的部分,右子树为大于根的部分,根据这个来递归判断。但是这里面有个要注意的地方,我们可以根据一些条件提前终止判断。比如,如果发现右子树依然有小于根的数字,那么一定是不成立的。

class Solution:
    def verifyPostorder(self, postorder: List[int]) -> bool:
        if len(postorder)<=1:
            return True
        root=postorder.pop()
        i=0
        while i<len(postorder) and postorder[i]<root:
            i+=1
        lefts=postorder[:i]
        rights=postorder[i:]
        if rights and min(rights)<root:
            return False

        return self.verifyPostorder(lefts) and self.verifyPostorder(rights)

11. 只出现一次的数字

首先很明显,我们有个通用算法,如果数组只有一个出现一次,其他都出现K次,那么只要位统计一下,把次数%K的位找到,然后还原出即可。

但是这个题目有个看起来很优美的解法,我们知道位统计的过程中,能够出现0,1,2,3....一些数字,但结合上面提到的,其实也就3个结果,0,1,2,表示统计结果mod 3之后的结果。这样的话,一个bit自然是不够的,我们需要两个bit来保存3个状态。

假设对于某一位,遇到多次,两个记录位变化应当是

b1 b2

0   0

0   1

1   0

0   0

....

b1=b1^b& ~b2

b2=b2^b& ~b1

12.最长不含重复字符的子字符串

此题以前总用双指针加数组判重做,属实有点low。这回采用动态规划。

我们考虑以s[i]结尾的字符能构造的最长不重复子串长度为dp[i]。

  1.  如果这个字符第一次出现,那么dp[i]=dp[i-1]+1,即可以直接追加到上一个子串末尾;
  2. 如果这字符上次出现的位置为j,但j比i-1-dp[i-1]小,也就是说j在上一个不重复子串的范围之外,那么可以直接追加到末尾,因为不会影响整体的不重复性,也就是dp[i-1]+1
  3. 反之,如果在上一个不重复子串之内,那么就要i-j了。

而且,很容易得知,可以利用哈希表获取前一个同字符位置,那么就很容易获得了。

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        if not s:
            return 0
        dp=[1]*len(s)
        last={}
        last[s[0]]=0
        ret=1
        for i in range(1,len(s)):
            j=last.get(s[i],-1)
            last[s[i]]=i
            if j==-1:
                dp[i]=dp[i-1]+1
            else:
                dp[i]=min(dp[i-1]+1,i-j)
            ret= max(ret,dp[i])
        return ret

12.位运算求和

这题就没意思了,首先用一个变量保存所有进位然后迭代相异或。而且要注意的是保存进位时,要先转换为无符号数。

class Solution {
public:
    int add(int a, int b) {
        while(b){
            int c=(unsigned int)(a&b)<<1;
            a^=b;
            b=c;
        }
        return a;
    }
};

 13.滑动窗口最大值

此题比较经典,主要考察队列的使用,首先构造一个递减序列,那么只要序列前面的值没有被窗口丢弃,就一直是最大值,依照这个思路,放置下标更容易一些。每次构造后都必须判断是否队头已经过期。边界的问题比较难搞。

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        if k==1:
            return nums
        from collections import deque
        q=deque()
        ret=[]
        for i in range(len(nums)):
            while q and nums[q[-1]]<=nums[i]:
                q.pop()
            q.append(i)

            if i-q[0]>=k:
                q.popleft()
            if i>=k-1:
                ret.append(nums[q[0]])
        return ret

14.最大队列

此题与最小值有异曲同工之妙,而且还结合了”滑动窗口最大值“的思路。

首先,需要两个队列,一个正常保存,一个保存最大值,需要注意的是,每次添加元素时,如果与最大队列里的值相等,那么添加进去,代表这个最大值出现了多少次,一旦出队时候恰好是这个值,那么就也出队一个最大值。所以关键点是递减栈一定是不严格递减 。

举个例子,入队顺序7 8 7 8 8 6 4,那么最大队列应该是8 8 8 6 4,而不是8 6 4,否则出队一次就把全部的8弄出去了。

class MaxQueue:

    def __init__(self):
        from collections import deque 
        self.q,self.mq=deque(),deque()


    def max_value(self) -> int:
        if not self.mq:
            return -1
        return self.mq[0]


    def push_back(self, value: int) -> None:
        self.q.append(value)
        while self.mq and self.mq[-1]<value:
            self.mq.pop()
        self.mq.append(value)



    def pop_front(self) -> int:
        if not self.q:
            return -1
        t=self.q.popleft()
        if t==self.mq[0]:
            self.mq.popleft()
        return t

15.概率

这个题目首先需要确定的是返回数组到底有多大。

迭代1次[1,2,3,4,5,6]

迭代2次[2,3....12]

也就是从k...6k一共5*k+1个数字。

假如某次迭代是从[k,k+1...6k]这5*k+1数字得到的概率为[Pk,Pk+1,Pk+2,.....P6k]

那么计算下一次迭代[k+1,....6k+6]的概率

每次循环6次,上次概率/6,循环加进新概率里,也就是

dp[n][j]=i=1∑6​dp[n−1][j−i]

所有第k+1次的数值,能由第k次的数字经过与1-6相加得到的,都会乘以1/6。

class Solution:
    def dicesProbability(self, n: int) -> List[float]:
        arr=[1/6]*6
        if n==1:
            return arr
        for i in range(2,n+1):
            tmp=[0]*(5*i+1)
            for j in range(len(arr)):
                for k in range(6):
                    tmp[j+k]+=arr[j]/6
            arr=tmp
        return arr

16.圆圈剩下的最后数字

此题仍然是一道数学题,我们考虑示例1,以追加一个复制数组模拟环:

迭代0:0,1,2,3,4,5,0,1,2,3,4,5... 

迭代1:3,4,5,0,3,4,5,0...

迭代2:0,3,4,0,3,4...

迭代3:0,3,0,3...

迭代4:3

可以看出迭代4中我们只要把3位置加上m mod 当前上一个迭代长度就得到上一个迭代的位置。

(0+3)%2=1

(1+3)%3=1

(1+3)%4=0

(0+3)%5=3

根据这个就很容易得到在原数组中的位置。

class Solution:
    def lastRemaining(self, n: int, m: int) -> int:
        b=2
        a=0
        while b<=n:
            a=(a+m)%b
            b+=1
        return a

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值