第6章 面试中的各项能力

参考:

  1. 所有offer题目的LeetCode链接及python实现
  2. github Target offer

面试题38:数字在排序数组中出现的次数

题目:统计一个数字在排序数组中出现的次数。例如输入排序数组{1,2,3,3,3,3,4,5}和数字3,由于3在这个数组中出现了4次,因此输出4。

思路梳理

二分查找算法:
GetFirstK用于寻找第一个k出现的位置;

  • 每一次将数组中间的位置与k比较
  • 如果data[mid]==k,判断data[mid-1]与k的关系,再决定是否进一步查找
  • 如果data[mid-1]==k,继续在data[:mid]中二分查找k
  • 如果data[mid-1]!=k,则index=mid即为所求。

GetLastK用于寻找最后一个k出现的位置;思路与上述类似。
GetFirstK和GetLastK都是用二分查找法在数组中查找一个合乎要求的数字,它们的时间复杂度都是O(logn),因此GetNumberOfK的总的时间复杂度也只有O(logn)。

# -*- coding:utf-8 -*-
class Solution:
    def GetNumberOfK(self, data, k):
        if data == [] or not k:
            return 0
        length = len(data)
        first = self.GetFirstK(data, k, 0, length-1)
        last = self.GetLastK(data, k, 0, length-1)
        
        return last-first+1 if first>-1 and last>-1 else 0
    
    def GetFirstK(self, data, k, start, end):
        if start>end:
            return -1
        mid = int((start + end)/2)
        if data[mid]<k:
            start = mid + 1
        elif data[mid]==k:
            if mid>0 and data[mid-1]==k:
                end = mid - 1
            else:
                return mid
        else:
            end = mid - 1
        return self.GetFirstK(data, k, start, end)
    
    def GetLastK(self, data, k, start, end):
        if start>end:
            return -1
        
        mid = int((start + end)/2)
        if data[mid]<k:
            start = mid+1
        elif data[mid]==k:
            if mid<end and data[mid+1]==k:
                start = mid+1
            else:
                return mid
        else:
            end = mid - 1
        return self.GetLastK(data, k, start, end)

面试题39:二叉树的深度

题目一:输入一棵二叉树的根结点,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

思路梳理

  • 如果一棵树只有一个结点,它的深度为1。
  • 如果根结点只有左子树而没有右子树,那么树的深度应该是其左子树的深度加1;
  • 同样如果根结点只有右子树而没有左子树,那么树的深度应该是其右子树的深度加1。
  • 如果既有右子树又有左子树,那该树的深度就是其左、右子树深度的较大值再加1。
  • 上述思路使用递归非常容易实现。

复杂度分析:

  • 时间复杂度:我们每个结点只访问一次,因此时间复杂度为 O(N), 其中 N 是结点的数量。
  • 空间复杂度:在最糟糕的情况下,树是完全不平衡的,例如每个结点只剩下左子结点,递归将会被调用 N 次(树的高度),因此保持调用栈的存储将是 O(N)。但在最好的情况下(树是完全平衡的),树的高度将是 log(N)。因此,在这种情况下的空间复杂度将是 O(log(N))。
class Solution(object):
    def maxDepth(self, root):
        if root is None:
            return 0
        else:
            leftD = self.maxDepth(root.left)
            rightD = self.maxDepth(root.right)
            return max(leftD, rightD)+1        

非递归版本:
使用栈存储已遍历节点和其对应的深度。
复杂度分析:

  • 时间复杂度:O(N)。
  • 空间复杂度:O(N)。
class Solution(object):
    def maxDepth(self, root):
        stack = []
        if root is not None:
            stack.append((1, root))
        else:
            return 0
        
        depth = 0
        while stack:
            # d, node = stack.pop()
            # depth = max(depth, d)
            # if node.left:
            #     stack.append((d+1, node.left))
            # if node.right:
            #     stack.append((d+1, node.right))
            #### 减少了判断条件,更快,
            current_depth, root = stack.pop()
            if root is not None:
            	# 当本次弹出的节点非空时,才更新最大深度
                depth = max(depth, current_depth)
                stack.append((current_depth + 1, root.left))
                stack.append((current_depth + 1, root.right))
        return depth

题目二:平衡二叉树

输入一棵二叉树的根结点,判断该树是不是平衡二叉树。如果某二叉树中任意结点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

思路梳理
  • 后序遍历的方式遍历二叉树的每一个结点,在遍历到一个结点之前我们就已经遍历了它的左右子树。
  • 只要在遍历每个结点的时候记录它的深度(某一结点的深度等于它到叶节点的路径的长度),
  • 就可以一边遍历一边判断每个结点是不是平衡的。

递归版本的实现:

class Solution(object):
    def maxDepth(self, root):
        if root is None:
            return 0
        else:
            leftD = self.maxDepth(root.left)
            rightD = self.maxDepth(root.right)
            return max(leftD, rightD)+1  
        
    def isBalanced(self, root):
        if root is None:
            return True
        if abs(self.maxDepth(root.left)-self.maxDepth(root.right))>1:
            return False
        else:
            return self.isBalanced(root.left) and self.isBalanced(root.right)

面试题40:数组中只出现一次的数字

题目:一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

思路梳理

  • 如果需要找出一个只出现一次的数字,则可以对数组中所有数字两两异或,由于其他出现两次的数字异或之后都抵消为零,所以最终结果即为出现一次的数字。
  • 针对两个只出现一次的数字,借用上述思路,可以将数组先分成两个子数组,使各自包含一个只出现一次的数字。
    1. 如何区分两个数组:将原始数组两两异或,最终得到的结果就是两个只出现一次的数字的异或结果。由于这两个数字肯定不一样,那么异或的结果肯定不为0,也就是说在这个结果数字的二进制表示中至少就有一位为1。我们在结果数字中找到第一个为1的位的位置,记为第n位
    2. 按照第n个比特位上数字是否为1,将数组划分为两个不相交的数组,其中各自包含一个只出现一次的数字。
    3. 使用找出一个只出现一次的数字的思路,分别在两个子数组中找出答案。
# -*- coding:utf-8 -*-
class Solution:
    def partition(self,array):
        n = 0
        for i in array:
            n ^= i
        # 找到异或结果中,最左边的1bit位的位置
        start = 0
        while n & 1 == 0 and start <= 32:
            start += 1
            n = n >> 1
        arrayA, arrayB = 0, 0 
        for i in array:
            # 判断a在start位上是否为1
            # 直接对找到的数字进行异或,得到最终所求数字
            if (i >> start)&1 == 1:
                arrayA ^= i
            else:
                arrayB ^= i
        return arrayA, arrayB
        
    # 返回[a,b] 其中ab是出现一次的两个数字
    def FindNumsAppearOnce(self, array):
        if array == [] or len(array)<2:
            return 
        elif len(array) == 2:
            return array
        return self.partition(array)

面试题41:和为s的两个数字VS和为s的连续正数序列

**题目一:**输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,输出任意一对即可。

思路梳理

  • 定义两个指针,ahead为较小(数组开头)的数字的下标,behind为较大(数组末尾)的数字的下标。
  • while循环继续的条件是ahead>behind。
  • 时间复杂度:O(N)

题目二:输入一个正数s,打印出所有和为s的连续正数序列(至少含有两个数)。例如输入15,由于1+2+3+4+5=4+5+6=7+8=15,所以结果打印出3个连续序列1~5、4~6和7~8。

思路梳理

定义两个序列指针,small与big,small指向较小数字即序列的开头,big指向较大数字即序列的结尾;

  • 如果当前nums[small:big]求和大于target_sum,则增大small即丢弃较小的值;
  • 如果当前nums[small:big]求和小于target_sum,则增大big即增加较大的值;
# -*- coding:utf-8 -*-
class Solution:
    def FindContinuousSequence(self, tsum):
        if tsum <= 2:
            return []
        small, big = 1, 2
        cur_sum = small + big
        ans = []
        # 定义中间数字
        middle = (tsum + 1) >> 1
        # while big < tsum:
        # 终止条件
        while small < middle:
            if cur_sum == tsum:
                ans.append(list(range(small,big+1)))
                cur_sum -= small
                small += 1
            elif cur_sum < tsum:
                big += 1
                cur_sum += big
            else:
                cur_sum -= small
                small += 1
                
        return ans

面试题42:翻转单词顺序 VS左旋转字符串

**题目一:**输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. “,则输出"student. a am I”。

思路梳理

  • 首先需要写一个reverse函数(见题目二的代码),把任何输入的字符串完全翻转。
  • 然后从前往后依次遍历新字符串,如果遇到空格,就把空格前的字符串用reverse翻转,添加空格,继续遍历。
  • 需要注意的是,如果新字符串结尾不是空格,当遍历到结尾的时候,前一个空格到结尾的字符串没有翻转,因此记得跳出遍历后,需要再完成一次翻转操作。

先使用s.split(' ') 将字符串分成多个单词,再使用 ' '.join(l[::-1]) 将分开的单词列表,按照从后到前的顺序连接起来,即可得到所求。

 # 直接利用Python的语句进行字符串的翻转
    def ReverseSentence2(self, s):
        l = s.split(' ')
        return ' '.join(l[::-1])

**题目二:**字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如输入字符串"abcdefg"和数字2,该函数将返回左旋转2位得到的结果"cdefgab"。

思路梳理

将前半段和后半段分别反转后,交换两段的位置即可。

  • 首先需要写一个reverse函数,把任何输入的字符串完全翻转。
  • 然后根据题目中给出的左旋转字符串的个数n,用全字符串长度length减去旋转字符串个数n,求得对于新的字符串应该在哪一位进行旋转,
  • 然后分别旋转前[:length-n]子串和[length-n:]子串,重新拼接两个子串即可。
# -*- coding:utf-8 -*-
class Solution:
    def LeftRotateString(self, s, n):
        if len(s) <= 0 or len(s) < n or n < 0:
            return ''
        strList= list(s)
        # 翻转字符串
        self.Reverse(strList)
        length = len(s)
        pivot = length - n
        frontList = self.Reverse(strList[:pivot])
        behindList = self.Reverse(strList[pivot:])
        resultStr = ''.join(frontList) + ''.join(behindList)
        return resultStr

    def Reverse(self, alist):
        if alist == None or len(alist) <= 0:
            return ''
        startIndex = 0
        endIndex = len(alist) - 1
        # 从数组的两端开始,依次交换数组元素,直到比较的对象位于中间为止
        while startIndex < endIndex:
            alist[startIndex], alist[endIndex] = alist[endIndex], alist[startIndex]
            startIndex += 1
            endIndex -= 1
        return alist
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值