第5章 优化时间和空间效率

参考:

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

5.2 时间效率

  • 递归的本质是把一个大的复杂问题分解成两个或者多个小的简单的问题。如果小问题中有相互重叠的部分,那么直接用递归实现虽然代码显得很简洁,但时间效率可能会非常差。
  • 同样是查找,如果是顺序查找需要O(n)的时间;如果输入的是排序的数组则只需要O(logn)的时间;如果事先已经构造好了哈希表,那查找在O(1)时间就能完成。

面试题29:数组中出现次数超过一半的数字

题目:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。

思路梳理

方法一:根据数组特点找出O(n)的算法

数组中有一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现次数的和还要多。因此我们可以考虑在遍历数组的时候保存两个值:

  • 一个是数组中的一个数字,
  • 一个是次数。
  • 当我们遍历到下一个数字的时候,如果下一个数字和我们之前保存的数字相同,则次数加1;
  • 如果下一个数字和我们之前保存的数字不同,则次数减1。
  • 如果次数为零,我们需要保存下一个数字,并把次数设为1。
  • 由于我们要找的数字出现的次数比其他所有数字出现的次数之和还要多,那么要找的数字肯定是最后一次把次数设为1时对应的数字。
class Solution:
    # 基于Partition函数的O(n)算法
    def MoreThanHalfNum_Solution(self, numbers):
        length = len(numbers)
        if length == 1:
            return numbers[0]
        if self.CheckInvalidArray(numbers, length):
            return 0

        times = 0
        for i in numbers:
            if not times:
                res = i
                times += 1
            if i == res:
                times += 1
            else:
                times -= 1
            
        return res if self.CheckMoreThanHalf(numbers, length, res) else 0
    
    # 检查输入的数组是否合法
    def CheckInvalidArray(self, numbers, length):
        InputInvalid = False
        if numbers == None or length <= 0:
            InputInvalid = True
        return InputInvalid
    # 检查查找到中位数的元素出现次数是否超过所有元素数量的一半
    def CheckMoreThanHalf(self, numbers, length, number):
        times = 0
        for i in range(length):
            if numbers[i] == number:
                times += 1
        if times*2 <= length:
            return False
        return True
方法二:基于Partition函数的O(n)算法
  1. 由于当前数组是非排序的,所以需要先进行排序,O(nlogn);
  2. 对于有序数组来说,中位数即 n/2 位置上的数字一定是频率超过一半的数字;
  3. 按照快速排序的思想,
    1. 选择一个基准pivot;
    2. 将小于pivot的数字放在 pivot 左边,将大于pivot的数字放在 pivot 右边;
    3. 如果本轮 pivot 的索引大于 n/2 ,则寻找的目标在 pivot 左边;否则,在 pivot 右边。
  4. 判断特殊情况:
    1. 输入数组为空;
    2. 不存在频率超过一半的数字。
class Solution:
    # 基于Partition函数的O(n)算法
    def MoreThanHalfNum_Solution(self, numbers):
        length = len(numbers)
        if length == 1:
            return numbers[0]
        if self.CheckInvalidArray(numbers, length):
            return 0

        middle = length >> 1
        start = 0
        end = length - 1
        index = self.Partition(numbers, length, start, end)
        while index != middle:
            if index > middle:
                end = index - 1
                index = self.Partition(numbers, length, start, end)
            else:
                start = index + 1
                index = self.Partition(numbers, length, start, end)
        result = numbers[middle]
        if not self.CheckMoreThanHalf(numbers, length, result):
            result = 0
        return result
    # 划分算法
    def Partition(self, numbers, length, start, end):
        if numbers == None or length <= 0 or start < 0 or end >= length:
            return None
        if end == start:
            return end
        pivotvlue = numbers[start]
        leftmark = start + 1
        rightmark = end

        done = False

        while not done:
            while numbers[leftmark] <= pivotvlue and leftmark <= rightmark:
                leftmark += 1
            while numbers[rightmark] >= pivotvlue and rightmark >= leftmark:
                rightmark -= 1

            if leftmark > rightmark:
                done = True
            else:
                numbers[leftmark], numbers[rightmark] = numbers[rightmark], numbers[leftmark]
        numbers[rightmark], numbers[start] = numbers[start], numbers[rightmark]
        return rightmark

    # 检查输入的数组是否合法
    def CheckInvalidArray(self, numbers, length):
        InputInvalid = False
        if numbers == None or length <= 0:
            InputInvalid = True
        return InputInvalid
    # 检查查找到中位数的元素出现次数是否超过所有元素数量的一半
    def CheckMoreThanHalf(self, numbers, length, number):
        times = 0
        for i in range(length):
            if numbers[i] == number:
                times += 1
        if times*2 <= length:
            return False
        return True

面试题30:最小的k个数

题目:输入n个整数,找出其中最小的k个数。例如输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

思路梳理

简单:先排序后输出
  • 把输入的n个整数排序,排序之后位于最前面的k个数就是最小的k个数。这种思路的时间复杂度是O(nlogn),面试官会提示我们还有更快的算法。
解法一:O(n)的算法,只有当我们可以修改输入的数组时可用

第一种方法是基于划分的方法,如果是查找第k个数字,第一次划分之后,划分的位置如果大于k,那么就在前面的子数组中进行继续划分,反之则在后面的子数组继续划分,时间复杂度O(n);

    def GetLeastNumbers_Solution(self, tinput, k):
        if tinput == None or len(tinput) < k or len(tinput) <= 0 or k <=0:
            return []
        n = len(tinput)
        start = 0
        end = n - 1
        index = self.Partition(tinput, n, start, end)
        while index != k-1:
            if index > k-1:
                end = index - 1
                index = self.Partition(tinput, n, start, end)
            else:
                start = index + 1
                index = self.Partition(tinput, n, start, end)
        output = tinput[:k]
        output.sort()
        return output
解法二:O(nlogk)的算法,特别适合处理海量数据

第二种方法是可以适用于海量数据的方法,该方法基于二叉树或者堆来实现,

  • 首先把数组前k个数字构建一个最大堆,
  • 然后从第k+1个数字开始遍历数组,
    • 如果遍历到的元素小于堆顶的数字,那么就交换当前数字和之前的堆顶,重新构造堆,继续遍历,
    • 如果大于堆顶,跳过
  • 最后剩下的堆就是最小的k个数,时间复杂度O(nlogk)。

面试题31:连续子数组的最大和

题目:输入一个整型数组,数组里有正数也有负数。数组中一个或连续的多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。

解法一:举例分析数组的规律

  1. 遍历数组,将遇到的每个数字加到 res 中,记录当前子数组的和;
    1. 记录或者更新当前的最大的子数组和;
    2. 如果当前的和小于或等于本数字,则代表当前的子数组并非最大子数组,所以子数组的起点应该从当前数字开始,之前累计的和也可以抛弃;
class Solution:
    def FindGreatestSumOfSubArray(self, array):
        if array == None or len(array) <= 0:
            return 0
        maxSum = array[0]
        s = array[0]
        length = len(array)
        for i in range(1,length):
            s += array[i]
            # 如果当前和小于等于当前数字,则抛弃数组中之前的数字
            if s <= 0:
                s = array[i]
            if s > maxSum:
                maxSum = s
        return maxSum
解法二:应用动态规划法

如果用函数f(i)表示以第i个数字结尾的子数组的最大和,那么我们需要求出max[f(i)],其中0≤i<n。我们可用如下递归公式求f(i):
子数组最大和的递推式
这个公式的意义:

  • 当以第i-1个数字结尾的子数组中所有数字的和小于0时,如果把这个负数与第i个数累加,得到的结果比第i个数字本身还要小,所以这种情况下以第i个数字结尾的子数组就是第i个数字本身(如表5.2的第3步)。
  • 如果以第i-1个数字结尾的子数组中所有数字的和大于0,与第i个数字累加就得到以第i个数字结尾的子数组中所有数字的和
  • 最后取 max[f(i)] 作为返回结果。
# -*- coding:utf-8 -*-
class Solution:
    def FindGreatestSumOfSubArray(self, array):
        if array == None or len(array) <= 0:
            return 0
        length = len(array)
        aList = [0]*length
        aList[0] = array[0]
        for i in range(1, length):
            # 如果当前和小于等于当前数字,则抛弃数组中之前的数字
            if aList[i-1]<=0:
                aList[i] = array[i]
            else:
                aList[i] = aList[i-1] + array[i]
        return max(aList)

面试题32:从1到n整数中1出现的次数

题目:输入一个整数n,求从1到n这n个整数的十进制表示中1出现的次数。例如输入12,从1到12这些整数中包含1 的数字有1,10,11和12,1一共出现了5次。

思路梳理

借用字符串

思路2:将1-n全部转换为字符串,只需要统计每个字符串中’1’出现的次数并相加即可

class Solution:
    def NumberOf1Between1AndN_Solution(self, n):
        count = 0
        for i in range(1,n+1):
            for i in str(i):
                if i == '1':
                    count += 1
        return count
以百位上1出现的次数为例子

设N = abcde ,其中abcde分别为十进制中各位上的数字。如果要计算百位上1出现的次数,它要受到3方面的影响:百位上的数字,百位以下(低位)的数字,百位以上(高位)的数字。

① 如果百位上数字为0,百位上可能出现1的次数由更高位决定。比如:12013,则可以知道百位出现1的情况可能是:100-199,1100-1199,2100-2199,,…,11100-11199,一共1200个。可以看出是由更高位数字(12)决定,并且等于更高位数字(12)乘以 当前位数(100)。

② 如果百位上数字为1,百位上可能出现1的次数不仅受更高位影响还受低位影响。比如:12113,则可以知道百位受高位影响出现的情况是:100-199,1100-1199,2100-2199,,…,11100-11199,一共1200个。和上面情况一样,并且等于更高位数字(12)乘以 当前位数(100)。但同时它还受低位影响,百位出现1的情况是:12100~12113,一共114个,等于低位数字(113)+1。

③ 如果百位上数字大于1(2-9),则百位上出现1的情况仅由更高位决定,比如12213,则百位出现1的情况是:100-199,1100-1199,2100-2199,,…,11100-11199,12100~12199,一共有1300个,并且等于更高位数字+1(12+1)乘以当前位数(100)。

def NumberOf1Between1AndN_Solution(self, N):
    #make sure that N is an integer
    N = int(N)
    #convert N to chars
    a = str(N)
    #the lenth is string a
    n = len(a)
    i = 0
    count = 0
    while (i < n):
       if(i == 0):
           if(int(a[i]) == 1 ):
               count += int(a[1:])+1
           elif(int(a[i]) > 1):
               count += 10 ** (n-1)
       elif(i == n - 1):
           if(int(a[i]) == 0):
               count += int(a[:n-1])
           else:
               count += int(a[:n-1]) + 1
       else:
           if(int(a[i]) == 0):
               count += int(a[:i]) * (10 ** (n - i - 1))
           elif(int(a[j]) == 1):
               count += int(a[:i]) * (10 ** (n - i - 1)) + int(a[i+1:]) + 1
           else:
               count += (int(a[:i]) + 1) * (10 ** (n - i -1))
       i += 1
    return count
方法一:O(n*logn)
  • 对每个数字都要做除法和求余运算以求出该数字中十位数和个位数上 1 出现的情况。
  • 如果输入数字n,n有O(logn)位,我们需要判断每一位是不是1,那么它的时间复杂度是O(n*logn)。当输入n非常大时,运算效率低。
方法二:从数字规律着手明显提高时间效率的解法

每次去掉最高位做递归,递归的次数和位数相同。一个数字n有O(logn)位,因此这种思路的时间复杂度是O(logn)

面试题33:把数组排成最小的数

题目:输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这3个数字能排成的最小数字321323。

思路梳理

  1. 所有数字组合之后的结果有 n!种。
  2. 优化:先对数组中的数字进行排序,然后串联数字;
    确定排序规则:比较两个数字,也就是给出两个数字m和n,需要确定一个规则判断m和n哪个应该排在前面,而不是仅仅比较这两个数字的值哪个更大。
    • 如果mn<nm,那么应该打印出mn,也就是m应该排在n的前面,定义此时m小于n;
    • 反之,如果nm<mn,我们定义n小于m。
    • 如果mn=nm,m等于n。
  3. 这种思路的时间复杂度与排序算法的时间复杂度相同,也就是 O ( n l o g n ) O(nlogn) O(nlogn).
  4. 避免数字溢出:把两个int型的整数拼接起来得到的数字可能会超出int型数字能够表达的范围。用字符串表示数字
    注意:
    快速排序中递归调用部分,注意索引的起点和终点的变化。
class Solution:
    def quickSort(self, nums, start, end):
        # 由于本算法是直接对数组进行改变,所以针对于非法输入的数组可以直接忽略
        if end > start:
            pivot = nums[-1]
            i = j = start
            while j < end:
                s1 = nums[j] + pivot
                s2 = pivot + nums[j]
                if s1 < s2:
                    nums[i], nums[j] = nums[j], nums[i]
                    i += 1
                j += 1
            # 交换pivot和i索引的元素, 此时的j指向pivot
            nums[i], nums[end] = nums[end], nums[i]
            # 递归调用,对左右两部分排序
            self.quickSort(nums, start, i-1)
            self.quickSort(nums, i+1, end)

    def PrintMinNumber(self, numbers):
        if not numbers:
            return ""
        str_list = []
        for n in numbers:
            str_list.append(str(n))
        length = len(numbers)
        st = 0
        en = length - 1
        self.quickSort(str_list, st, en)
        # 使用 python 自带的方法,将字符串列表连接起来
        return ''.join(str_list)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值