常见的时间复杂度

O ( log ⁡ N ) \mathcal{O}(\log N) O(logN)

题目特征:

  1. 数据是有序或者相对有序
  2. 每次操作可以排除掉一半的可能性

有序数据

代表场景:

  • 二分查找
    • 普通二分查找
    • 二分下界查找
    • 二分上界查找

我作了首诗,保你闭着眼睛也能写对二分查找

二分查找算法如何运用?我和快手面试官进行了深入探讨…

搜索一个元素时,搜索区间两端闭
while条件带等号,否则需要打补丁
if相等就返回,其他事情甭操心
mid必须加减一,因为区间两端闭
while结束就凉了,凄凄惨惨返-1

搜索左右区间时,搜索区间要阐明
左闭右开最常见,其余逻辑便自明
while要用小于号,这样才能不漏掉
if相等别返回,利用mid锁边界

普通二分查找:左闭右闭
上下界查找: 左闭右开
while <

nums = [1, 1, 3, 6, 6, 6, 7, 8, 8, 9]


def binary_search(nums, target):
    '''找一个数'''
    l = 0
    r = len(nums) - 1
    while l <= r:
        mid = (l + r) // 2
        # 如果是Java Cpp
        # mid = l + (r - l) // 2
        if nums[mid] == target:
            return mid
        elif nums[mid] < target:
            l = mid + 1
        elif nums[mid] > target:
            r = mid - 1
    return -1


print("binary_search", binary_search(nums, 6))


def lower_bound(nums, target):
    '''找左边界'''
    l = 0
    r = len(nums)
    while l < r:
        mid = (l + r) // 2
        if nums[mid] == target:
            r = mid
        elif nums[mid] < target:
            l = mid + 1
        elif nums[mid] > target:
            r = mid
    # 对未命中情况进行后处理
    if l == len(nums):
        return -1
    return l if nums[l] == target else -1


print("lower_bound", lower_bound(nums, 6))
print("lower_bound", lower_bound(nums, 2))


def upper_bound(nums, target):
    '''找右边界'''
    l = 0
    r = len(nums)
    while l < r:
        mid = (l + r) // 2
        if nums[mid] == target:
            l = mid + 1
        elif nums[mid] < target:
            l = mid + 1
        elif nums[mid] > target:
            r = mid
    if l == 0:
        return -1
    return l - 1 if nums[l - 1] == target else -1


print("upper_bound", upper_bound(nums, 100))
print("upper_bound", upper_bound(nums, -1))
print("upper_bound", upper_bound(nums, 2))
print("upper_bound", upper_bound(nums, 6))

相对有序数据

数据是相对有序的,可以通过一些tricky做到二分查找,这里列举3道题

咱们讨论的第一题相对最简单:

在这里插入图片描述

class Solution:
    def minArray(self, numbers: List[int]) -> int:
        l = 0
        r = len(numbers) - 1
        while l < r:
            mid = (l + r) // 2
            if numbers[l] >= numbers[r]:
                if numbers[mid] == numbers[r]:
                    l += 1
                elif numbers[mid] < numbers[r]:
                    r = mid
                else:
                    l = mid + 1
            else:
                return numbers[l]
        return numbers[r]

第二题相对难一些:

在这里插入图片描述

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        n = len(nums)
        l = 0
        r = n - 1
        while l <= r:
            mid = (l + r) // 2
            if nums[mid] == target:
                return mid
            if nums[l] < nums[mid] or l==mid:
                if nums[l] <= target < nums[mid]:
                    r = mid - 1
                else:
                    l = mid + 1
            else:  # 写成了 <
                if nums[mid] < target <= nums[r]:
                    l = mid + 1
                else:
                    r = mid - 1
        return -1

第三题在第二题的基础上去掉了【互不相同】这个限定条件,这是常见的套路(反正我是第3次见到了),遇到这种场景直接加个线性搜索就好了:

注意:这题的时间复杂度分上下界,如果所有元素相同的情况下,时间复杂度为 O ( N ) \mathcal{O}(N) O(N),如果所有元素各不相同,变为上题的情形,为 O ( log ⁡ N ) \mathcal{O}(\log N) O(logN)

时间复杂度分情况讨论的场景,还有很多,举几个例子,快速排序相对有序时,时间复杂度为 O ( N 2 ) \mathcal{O}(N^2) O(N2)(可以理解为二叉树退化为链表),其他为 O ( N log ⁡ N ) \mathcal{O}(N\log N) O(NlogN)。插入排序在元素升序相对有序时,时间复杂度为 O ( N ) \mathcal{O}(N) O(N),平均时间复杂度为 O ( N 2 ) \mathcal{O}(N^2) O(N2)

在这里插入图片描述

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        n = len(nums)
        l = 0
        r = n - 1
        while l <= r:
            mid = (l + r) // 2
            if nums[mid] == target:
                return True
            # 相比于原版就加了这个if判断
            if nums[l] == nums[mid]:
                l += 1
                continue
            if nums[l] < nums[mid] or l==mid:
                if nums[l] <= target < nums[mid]:
                    r = mid - 1
                else:
                    l = mid + 1
            else:  # 写成了 <
                if nums[mid] < target <= nums[r]:
                    l = mid + 1
                else:
                    r = mid - 1
        return False

每次操作排除一半可能性

  • 二叉搜索树
    • 二叉搜索树的插入
    • 二叉搜索树的查找
    • 二叉搜索树的删除
  • 二叉树的相关算法
    • 求完全二叉树的高度
  • 可以转为为二分查找的问题

等等,需要自己识别


二叉搜索树

判断二叉树是否合法

面试题 04.05. 合法二叉搜索树

98. 验证二叉搜索树

我们通过使用辅助函数,增加函数参数列表,在参数中携带额外信息,将这种约束传递给子树的所有节点,这也是二叉树算法的一个小技巧吧。

class Solution:
    def isValidBST(self, root: TreeNode) -> bool:
        return self._isValidBST(root, None, None)

    def _isValidBST(self, root: TreeNode, min_: TreeNode, max_: TreeNode):
        if root is None:
            return True
        if min_ is not None and root.val <= min_.val:
            return False
        if max_ is not None and root.val >= max_.val:
            return False
        return self._isValidBST(root.left, min_, root) and \
               self._isValidBST(root.right, root, max_)
BST的查找

700. 二叉搜索树中的搜索

class Solution:
    def searchBST(self, root: TreeNode, val: int) -> TreeNode:
        if root is None or root.val == val:
            return root
        if root.val < val:
            return self.searchBST(root.right, val)
        else:
            return self.searchBST(root.left, val)
BST的插入

701. 二叉搜索树中的插入操作

class Solution:
    def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode:
        if root is None:
            return TreeNode(val)
        if root.val < val:
            root.right = self.insertIntoBST(root.right, val)
        else:
            root.left = self.insertIntoBST(root.left, val)
        return root
BST的删除

450. 删除二叉搜索树中的节点

  1. 叶子结点(0个孩子) → 当场去世
  2. 1个孩子 → 让孩子接替自己位置
  3. 两个孩子 → 找到左子树中最大的结点右子树最小结点接替自己

class Solution:
    def deleteNode(self, root: TreeNode, key: int) -> TreeNode:
        if root is None:
            return None
        if root.val == key:
            # 一举解决了情况1和情况2
            if root.left is None:
                return root.right
            if root.right is None:
                return root.left
            # 情况3
            min_node = self.find_min(root.right) # root.right 写错
            root.val = min_node.val
            root.right = self.deleteNode(root.right, min_node.val)
        elif root.val > key: # 左右顺序写错
            root.left = self.deleteNode(root.left, key)
        elif root.val < key:
            root.right = self.deleteNode(root.right, key)

        return root

    def find_min(self, root: TreeNode) -> TreeNode:
        while root.left:
            root = root.left
        return root

可以转为为二分查找的问题

875. 爱吃香蕉的珂珂

本质是二分查找找左边界

需要注意的是时间复杂度的确定

O ( N log ⁡ W ) O(N\log W) O(NlogW),其中N是香蕉堆的数量,W是最大香蕉堆的大小

其中 log ⁡ W \log W logW表示了二分查找的复杂度,
O ( N ) O(N) O(N) 表示了每次查找进行cur_H计算的时间复杂度。

class Solution:
    def minEatingSpeed(self, piles: List[int], H: int) -> int:
        def get_H(k: int):
            return sum([ceil(pile / k) for pile in piles])

        l = 1
        r = max(piles)
        while l < r:
            mid = (l + r) // 2
            cur_H = get_H(mid)
            if cur_H == H:
                r = mid  # 往左边逼近
            elif cur_H < H:
                r = mid
            elif cur_H > H:
                l = mid + 1

        return l

1011. 在 D 天内送达包裹的能力

引人深思的一题,告诉我们当无法通过case的时候,需要仔细再看看题设条件

算法题,首先考的是语文/英语,然后才是算法

def get_D(weights, w):
    D = 0
    cur_weight = 0
    for weight in weights:
        if cur_weight + weight > w:
            cur_weight = 0
            D += 1
        cur_weight += weight
    return D + 1


class Solution:
    def shipWithinDays(self, weights: List[int], D: int) -> int:
        l = max(weights)  # fixme 错在这
        r = sum(weights)
        while l < r:
            mid = (l + r) // 2
            cur_D = get_D(weights, mid)
            if cur_D == D:
                r = mid
            elif cur_D < D:
                # 天数过少,减少最大载重,天数增加
                r = mid
            elif cur_D > D:
                l = mid + 1
        return l

O ( log ⁡ N ⋅ log ⁡ N ) \mathcal{O}(\log N \cdot \log N) O(logNlogN)

O ( N ) \mathcal{O}(N) O(N)

O ( N log ⁡ N ) \mathcal{O}(N\log N) O(NlogN)

O ( N 2 ) \mathcal{O}(N^2) O(N2)

O ( N k ) \mathcal{O}(N^k) O(Nk)

O ( N ! ) \mathcal{O}(N!) O(N!)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值