剑指offer 5.3 时间效率与空间效率的平衡
参考:
摘要
以空间换时间:分配少量的辅助空间来保存计算的中间结果以提高时间效率;
以时间换空间:
在n个无序的元素里做查找操作,需要O(n)的时间。
但如果我们把这些元素放进一个哈希表,那么在哈希表内就能实现O(1)的查找。
但同时实现一个哈希表是有空间消耗的,是不是值得以多消耗空间为前提来换取时间性能的提升,我们需要根据实际情况仔细权衡。
面试题34:丑数
题目:我们把只包含因子2、3和5的数称作丑数(Ugly Number)。求按从小到大的顺序的第1500个丑数。例如6、8都是丑数,但14不是,因为它包含因子7。习惯上我们把1当做第一个丑数。
思路梳理
- 判断一个数字是否丑数
class Solution(object):
def isUgly(self, num):
if num <= 0:
return False
while num%2 == 0:
num /= 2
while num%3 == 0:
num /= 3
while num%5 == 0:
num /= 5
return num == 1
- 寻找第index个丑数
- 依次将数字递增,判断每个数字是否丑数,消耗大量时间,效率不高;
- 创建数组保存已经找到的丑数,用空间换时间的解法:根据丑数的定义,丑数应该是另一个丑数乘以2、3或者5的结果(1除外)。因此我们可以创建一个数组,里面的数字是排好序的丑数,每一个丑数都是前面的丑数乘以2、3或者5得到的。
- 和第一种思路相比,第二种思路不需要在非丑数的整数上做任何计算,因此时间效率有明显提升。
- 第二种算法由于需要一个数组保存已经生成的丑数,从而增加了空间消耗。如果是求第1500个丑数,将创建一个能容纳1500个丑数的数组,这个数组占内存6KB。
- 而第一种思路没有这样的内存开销。总的来说,第二种思路相当于用较小的空间消耗换取了时间效率的提升。
# 第二种思路的实现
class Solution(object):
def nthUglyNumber(self, n):
if n == None and len(n) <= 0:
return 0
next_idx = 1
# 把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
uglyNums = [1]*n
index2, index3, index5 = 0, 0, 0
while next_idx < n:
# 每个数字所对应的最小丑数的位置,并且分别乘以2、3、5,再取最小值,作为序列的下一个值
minVal = min(uglyNums[index2]*2,uglyNums[index3]*3,uglyNums[index5]*5)
uglyNums[next_idx] = minVal
# 记下乘以某个数得到的丑数的位置
while uglyNums[index2]*2<=minVal:
index2 += 1
while uglyNums[index3]*3<=minVal:
index3 += 1
while uglyNums[index5]*5<=minVal:
index5 += 1
next_idx += 1
return uglyNums[-1]
面试题35:第一个只出现一次的字符
题目:在字符串中找出第一个只出现一次的字符。如输入"abaccdeff",则输出’b’。
思路梳理
两次遍历,第一次统计每个字符与其出现的次数,保存在哈希表中,键是字符,值是字符出现的次数;
- 时间复杂度:2*O(n)
- 空间复杂度:256个字符占1K的空间,由于数组的大小是个常数,所以算法的空间复杂度是O(1)。
LeetCode 387.未解
class Solution(object):
def firstUniqChar(self, s):
if not s or len(s) <= 0:
return -1
if len(s) == 1:
return s
m = {}
# s = list(s)
for c in s:
if c not in m.keys():
m[c] = 1
else:
m[c] += 1
# idx = -1
for k, v in m.items():
# idx += v
if v == 1:
return s.find(k)
return -1
相关题目
举一反三:
如果需要判断多个字符是不是在某个字符串里出现过或者统计多个字符在某个字符串中出现的次数,我们可以考虑基于数组创建一个简单的哈希表。这样可以用很小的空间消耗换来时间效率的提升。
面试题36:数组中的逆序对
题目:在数组中的两个数字如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
思路梳理
- 先用两个指针分别指向两个子数组的末尾,并每次比较两个指针指向的数字。
- 如果第一个子数组中的数字大于第二个子数组中的数字,则构成逆序对,并且逆序对的数目等于第二个子数组中剩余数字的个数。
- 如果第一个数组中的数字小于或等于第二个数组中的数字,则不构成逆序对(如图5.2(b)所示)。
- 每一次比较的时候,都把较大的数字从后往前复制到一个辅助数组中去,确保辅助数组中的数字是递增排序的。
- 在把较大的数字复制到辅助数组之后,把对应的指针向前移动一位,接下来进行下一轮比较。
总结统计逆序对的过程:
- 先把数组分隔成子数组,先统计出子数组内部 (如5.1b所示)的逆序对的数目,
- 然后再统计出两个相邻子数组之间的逆序对的数目。
- 在统计逆序对的过程中,还需要对数组进行排序。
- 如果对排序算法很熟悉,我们不难发现这个排序的过程实际上就是归并排序。
实现一
定义一个新数组copy,保存排好序的data数组
# -*- coding:utf-8 -*-
class Solution:
def InversePairsCore(self, data, copy, start, end):
if start == end:
copy[start] = data[start]
return 0
length = len(data)
mid = int((start+end)/2)
left = self.InversePairsCore(copy, data, start, mid)
right = self.InversePairsCore(copy, data, mid+1, end)
i = mid
j = end
k = end
res = 0
while i>=start and j>=mid+1:
if data[i] > data[j]:
res += j-mid
copy[k] = data[i]
i -= 1
k -= 1
else:
copy[k] = data[j]
j -= 1
k -= 1
while i >= start:
copy[k] = data[i]
i-=1
k-=1
while j >= mid+1:
copy[k] = data[j]
j -= 1
k -= 1
return left + right + res
def InversePairs(self, data):
if data==None or len(data) <= 1:
return 0
length = len(data)
data_copy = [i for i in data]
res = self.InversePairsCore(data, data_copy, 0, length-1)
return res%1000000007
实现二
超时
# -*- coding:utf-8 -*-
class Solution:
# 定义全局变量
# res = 0
def Merge(self, data1, data2):
add = []
i = 0
j = 0
while i<len(data1) and j<len(data2):
if data1[i] < data2[j]:
add.append(data1[i])
i += 1
else:
add.append(data2[j])
j += 1
add += data1[i:]
add += data2[j:]
return add
def MergeSort(self, data):
if len(data) <= 1:
return data
length = len(data)
mid = int(length//2)
left = self.MergeSort(data[:mid])
right = self.MergeSort(data[mid:])
return self.Merge(left, right)
def InversePairs(self, data):
if len(data) <= 1:
return 0
length = len(data)
mid = int(length//2)
sort_data = self.MergeSort(data)
count = 0
i = 0
while len(sort_data) > i:
count += data.index(sort_data[i])
data.remove(sort_data[i])
i += 1
return count%1000000007
总结:
归并排序的时间复杂度是
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),比最直观的
O
(
n
2
)
O(n^2)
O(n2)要快,但同时归并排序需要一个长度为 n 的辅助数组,相当于我们用
O
(
n
)
O(n)
O(n) 的空间消耗换来了时间效率的提升,因此这是一种用空间换时间的算法。
- 边界值测试(输入的数组中只有两个数字、数组的数组只有一个数字)
- 特殊输入测试(表示数组的指针为NULL指针)。
面试题37:两个链表的第一个公共结点
题目:输入两个链表,找出它们的第一个公共结点。链表结点定义如下:
思路梳理
- 蛮力法:O(mn),m、n分别为两个链表的长度
- 使用两个辅助栈,分别保存两个链表的元素,然后依次弹出栈顶元素“后进先出”,即按照链表的从后向前顺序,比较它们各自元素的值是否相同;直到找到最后一个相同的元素。
- 先遍历两个链表,得到它们各自的长度;跳过较长链表前面多余的元素;依次比较两个链表对应位置上值的大小,直到找到第一个相同的元素。
# -*- coding:utf-8 -*-
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def FindFirstCommonNode(self, pHead1, pHead2):
len1 = self.GetListLen(pHead1)
len2 = self.GetListLen(pHead2)
m = abs(len2 - len1)
if len1 > len2:
while m:
pHead1 = pHead1.next
m -= 1
elif len1 < len2:
while m:
pHead2 = pHead2.next
m -= 1
while pHead1 != None and pHead2 != None and pHead1 != pHead2:
pHead1 = pHead1.next
pHead2 = pHead2.next
return pHead1
def GetListLen(self,listnode):
len1 = 0
while listnode != None:
len1+=1
listnode = listnode.next
return len1