21.合并两个有序链表
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
思路:
题目要求:新链表是通过拼接给定的两个链表的所有节点组成的,所以不能使用一个新的链表。使用dumb帮助最后的返回,使用cur连接所有链表节点,不重不漏。
代码:
class ListNode(object):
def __init__(self, x):
self.val = x
self.next = None
class Solution(object):
def mergeTwoLists(self, l1, l2):
"""
:type l1: ListNode
:type l2: ListNode
:rtype: ListNode
"""
# 使用哑结点,一般还需要一个cur与之配合
dumb = ListNode(0)
cur = dumb
while l1 != None and l2 != None:
if l1.val < l2.val:
cur.next = l1
cur = cur.next
l1 = l1.next
else:
cur.next = l2
cur = cur.next
l2 = l2.next
if l1:
cur.next = l1
return dumb.next
if l2:
cur.next = l2
return dumb.next
分析:
时间复杂度:O(n), 线性扫描一遍l1和l2
空间复杂度:O(1), 常数空间储存dumb和cur即可
22.括号生成
给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。
示例:
例如,给出 n = 3,生成结果为:
[
“((()))”,
“(()())”,
“(())()”,
“()(())”,
“()()()”
]
思路:
回溯法:使用回溯法穷举所有的可能组合,回溯的过程中,根据条件去掉了很多不可能的情况,回溯法不是简单的穷举法
回溯法一般使用递归实现,参数选择很重要,详情可见回溯算法超通俗易懂详尽分析和例题
代码:
class Solution:
def generateParenthesis(self, n):
result = []
ans = ""
l = 0
r = 0
self.parenthesis(n, l, r, ans, result)
return result
def parenthesis(self, n, l, r, ans, result):
"""
:param n: 一共几对括号
:param l: ans中左括号的数量
:param r: ans中右括号的数量
:param ans: 当前的括号组合
:param result: 符合条件的括号组合
:return:
"""
if l == n and r == n:
result.append(ans)
return
else:
# 有两种情况可以添加括号,一种是l<n时,添加左括号,另一种是r<l时,添加右括号
if l < n:
self.parenthesis(n, l+1, r, ans+"(", result)
if r < l:
self.parenthesis(n, l, r+1, ans+")", result)
分析:
本题的复杂度分析叫复杂,下面是leetcode题解的分析:
23.合并K个排序链表
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
示例:
示例:
输入:
[
1->4->5,
1->3->4,
2->6
]
输出: 1->1->2->3->4->4->5->6
思路:
本题的难点在于时间复杂度分析,主要分为k是常数和k与n(n是链表的总长度)同阶两种情况:
方式一:将链表中所有元素全排序,O(nlogn),时间复杂度与k无关
方法二:线性扫描每一个链表头,找出最小的节点,时间复杂度O(kn), k与n同阶时,O(n2)
方式三:将每个链表头的结点构建成一个小顶堆O(k),每个取出堆顶元素,然后将该堆顶元素对应的链表的头元素插入堆O(logk),一共是n次,时间复杂度为O(k)+O(nlogk),当k与n同阶时,为O(n)+O(nlogn)
方式四:分治法,每次合并两个链表, 时间复杂度是O(nlogk), 当k与n同阶时,O(nlogn)
综上,方式三和方式四比较好
代码:
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution(object):
def mergeKLists(self, lists):
"""
:type lists: List[ListNode]
:rtype: ListNode
"""
# 使用分治法进行k个链表的合并
if len(lists) == 0:
return None
elif len(lists) == 1:
return lists[0]
elif len(lists) == 2:
return self.merge_two_lists(lists[0], lists[1])
mid = len(lists) // 2
l1 = lists[:mid]
l2 = lists[mid:]
return self.merge_two_lists(self.mergeKLists(l1), self.mergeKLists(l2))
def merge_two_lists(self, l1, l2):
dumb = ListNode(0)
cur = dumb
while l1 != None and l2 != None:
if l1.val < l2.val:
cur.next = l1
cur = cur.next
l1 = l1.next
else:
cur.next = l2
cur = cur.next
l2 = l2.next
if l1:
cur.next = l1
if l2:
cur.next = l2
return dumb.next
分析:
时间复杂度:
空间复杂度:
24.两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例:
示例:
给定 1->2->3->4, 你应该返回 2->1->4->3.
思路:
首先想明白整个交换的过程,一共涉及三个指针l,cur,r,每次交换由两部分组成,一是改变指针的连接顺序,二是移动指针的位置
代码:
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution(object):
def swapPairs(self, head):
"""
:type head: ListNode
:rtype: ListNode
"""
if head == None:
return None
dumb = ListNode(0)
cur = dumb
# 初始化三个指针
l = cur
cur.next = head
cur = cur.next
r = cur.next
while cur != None and cur.next != None :
# 节点连接改变
tmp = r.next
cur.next = tmp
r.next = cur
l.next = r
# 移动三个指针
l = cur
cur = cur.next
if cur != None:
r = cur.next
return dumb.next
分析:
时间复杂度:O(n)
空间复杂度:O(1)
25.k个一组翻转链表
给出一个链表,每 k 个节点一组进行翻转,并返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么将最后剩余节点保持原有顺序。
示例:
示例 :
给定这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
说明 :
你的算法只能使用常数的额外空间。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
思路:
同上题类似,使用l, cur, r三个指针进行位置变换,其中,cur和r每轮变换
代码:
class Solution(object):
def reverseKGroup(self, head, k):
"""
:type head: ListNode
:type k: int
:rtype: ListNode
"""
if head == None:
return None
dumb = ListNode(0)
l = dumb
cur = head
r = cur.next
l.next = cur
while True:
# 先判断包括cur以及之后的k个节点是否存在, 不存在则返回
flag_node = cur
for _ in range(k):
if flag_node == None:
return dumb.next
flag_node = flag_node.next
# 进行k个一组的翻转过程, 一共k-1步
new_l = cur
for _ in range(k - 1):
tmp = r.next
r.next = cur
cur = r
r = tmp
# 进行连接和指针位置变换
l.next = cur
l = new_l
cur = r
l.next = cur
if cur:
r = cur.next
分析:
时间复杂度:O(n)
空间复杂度:O(1)
26.删除排序数组中的重复项
给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
示例:
示例 1:
给定数组 nums = [1,1,2],
函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,0,1,1,1,2,2,3,3,4],
函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
思路:
使用指针i,j,指针i遍历数组,如果j和i不一样且j和i的值不一样,那么j+=1, nums[j] = nums[i]。
代码:
class Solution(object):
def removeDuplicates(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if len(nums) <= 1:
return len(nums)
j = 0
for i in range(len(nums)):
if i == j: # i和j位于同一位置时,i+=1
continue
if nums[j] != nums[i]: # nums[j] != nums[i]时,j += 1 然后nums[j] = nums[i]
j += 1
nums[j] = nums[i]
return j + 1
分析:
时间复杂度:O(n)
空间复杂度:O(1)
27.移除元素
给定一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例:
示例 1:
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,1,2,2,3,0,4,2], val = 2,
函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
注意这五个元素可为任意顺序。
你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
思路:
同上题类似,使用两个指针i,j,先将指针j移动到第一个val元素处,然后i从j+1位置开始移动如果i处的值和val不一样,就交换ij的值,然后继续移动j
代码:
class Solution(object):
def removeElement(self, nums, val):
"""
:type nums: List[int]
:type val: int
:rtype: int
"""
if nums == []:
return 0
for j in range(len(nums)):
if nums[j] != val:
continue
for i in range(j+1, len(nums)):
if nums[i] != val:
nums[j], nums[i] = nums[i], nums[j]
break
if i == len(nums) - 1: # 当i到达nums最后一个元素,且nums[i]==val时,返回j
return j
# 退出上面循环时,判断nums的最后一个元素与val的关系
if nums[-1] != val:
return len(nums)
else:
return len(nums) - 1
分析:
时间复杂度:O(n)
空间复杂度:O(1)
28.实现strStr()
实现 strStr() 函数。
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例:
示例 1:
输入: haystack = “hello”, needle = “ll”
输出: 2
示例 2:
输入: haystack = “aaaaa”, needle = “bba”
输出: -1
说明:
当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。
对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。
思路:
比较简单的方法是:遍历haystack的所有的字符(其实不是所有的字符,是range(len(haystack) - len(needle) + 1)),与needle相比较,第一个字符匹配,尝试匹配第二个字符…这样的时间复杂度比较高,如果想要减少时间复杂度,可是牺牲空间复杂度,储存一些外部表,用以在haystack跳转遍历,详情可以见数据结构,字符串匹配一章。
代码:
class Solution(object):
def strStr(self, haystack, needle):
"""
:type haystack: str
:type needle: str
:rtype: int
"""
if needle == "":
return 0
for i in range(len(haystack) - len(needle) + 1): #
for j in range(len(needle)):
if haystack[i+j] != needle[j]: # haystack[i+j] != needle[j]结束内循环
break
elif j == len(needle) - 1: # haystack[i+j] == needle[j]并且j是needle的最后一位时,返回i
return i
return -1
分析:
时间复杂度:O(nm)(n是haystack的长度,m是needle的长度), 如果使用储存表的形式,可以将时间复杂度优化至O(n),同时的空间复杂度为O(m)
空间复杂度:O(1)
29.两数相除
给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。
返回被除数 dividend 除以除数 divisor 得到的商。
示例:
示例 1:
输入: dividend = 10, divisor = 3
输出: 3
示例 2:
输入: dividend = 7, divisor = -3
输出: -2
说明:
被除数和除数均为 32 位有符号整数。
除数不为 0。
假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−231, 231 − 1]。本题中,如果除法结果溢出,则返回 231 − 1。
思路:
关键点:使用位运算代替乘法,除法和乘方运算
代码:
class Solution(object):
def divide(self, dividend, divisor):
"""
:type dividend: int
:type divisor: int
:rtype: int
"""
if dividend == 0:
return 0
# 先判断dividend和divisor是否同号,结果1先按两个正数来做,如果两者异号,结果为(-结果1 - 1)(LEETCODE的结果不一样,他认为结果为-结果1)
negative = dividend ^ divisor < 0
dividend = abs(dividend)
divisor = abs(divisor)
# 主循环,找到diviend大于等于divisor右移最大的位数,此时说明diviend至少包含1 << i个divisor, 则dividend -= divisor << i和result += 1 << i
result = 0
while dividend >= divisor:
for i in range(31, -1, -1):
if dividend >> i >= divisor:
result += 1 << i
dividend -= divisor << i
break
# 判断result范围
if (result >= 1 << 31 and not negative) or (result > 1 << 31 + 1 and negative):
return (1 << 31) - 1
return -result if negative else result
分析:
时间复杂度:O(1)
空间复杂度:O(1)
30.串联所有单词的子串
给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。
示例:
示例 1:
输入:
s = “barfoothefoobarman”,
words = [“foo”,“bar”]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 “barfoor” 和 “foobar” 。
输出的顺序不重要, [9,0] 也是有效答案。
示例 2:
输入:
s = “wordgoodgoodgoodbestword”,
words = [“word”,“good”,“best”,“word”]
输出:[]
思路:
方式一:s的长度为n,word的长度为s,一共t个word,st = m, 则将s分为n-m+1个字符串,然后子字符串与words进行匹配,这是需要使用递归的方式进行匹配
代码:
class Solution(object):
def findSubstring(self, s, words):
"""
:type s: str
:type words: List[str]
:rtype: List[int]
"""
result = []
if s == "" or words == []:
return result
words_length =len(words) * len(words[0])
for index in range(len(s)-words_length+1):
flag = self.match_substring(index, s, words.copy()) # 注意传入的是words的复制,否则words会越来越小
if flag:
result.append(index)
return result
def match_substring(self, index, s, words):
if words == []:
return True
word_length = len(words[0])
s_word = s[index: index+word_length]
if s_word in words:
words.remove(s_word)
return self.match_substring(index+word_length, s, words) # 这里需要加return
else:
return False
分析:
时间复杂度:最坏的情况下一共有n-m+1次比较,每次比较最坏m次,则时间复复杂度为O((n-m)m)
空间复杂度:O(1),需要常数空间进行储存数据