数组
1. 两数之和 - 力扣(LeetCode)
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
func twoSum(_ nums: [Int], _ target: Int) -> [Int] {
var dict = [Int: Int]()
for (i, num) in nums.enumerated() {
if let lastIndex = dict[target - num] {
return [lastIndex, i]
} else {
dict[num] = i
}
}
fatalError("No valid output")
}
/*
* 哈希表
* 时间复杂度:O(N),其中 N 是数组中的元素数量。对于每一个元素 x,我们可以 O(1) 地寻找 target - x。
* 空间复杂度:O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。
*/
字符串
344. 反转字符串 - 力扣(LeetCode)
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。 不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
func reverseString1(_ s: inout [Character]) {
// 双指针 - 元组
var l = 0
var r = s.count - 1
while l < r {
(s[l], s[r]) = (s[r], s[l])
l += 1
r -= 1
}
}
func reverseString2(_ s: inout [Character]) {
// 双指针 - 库函数
var j = s.count - 1
for i in 0 ..< Int(Double(s.count) * 0.5) {
s.swapAt(i, j)
j -= 1
}
}
/*
* 双指针
* 时间复杂度:O(N),其中 N 为字符数组的长度。一共执行了 N/2 次的交换。
* 空间复杂度:O(1)。只使用了常数空间来存放若干变量。
*/
链表
141. 环形链表 - 力扣(LeetCode)
给你一个链表的头节点 head ,判断链表中是否有环。
func hasCycle(_ head: ListNode?) -> Bool {
var slow = head
var fast = head
while fast != nil && fast!.next != nil {
slow = slow!.next
// 快指针两倍速前进
fast = fast!.next!.next
if slow === fast {
return true
}
}
return false
}
/*
* 快慢指针
* 时间复杂度:O(N),其中 N 是链表中的节点数
* - 当链表中不存在环时,快指针将先于慢指针到达链表尾部,链表中每个节点至多被访问两次。
* - 当链表中存在环时,每一轮移动后,快慢指针的距离将减小一。而初始距离为环的长度,因此至多移动 N 轮
* 空间复杂度:O(1)。我们只使用了两个指针的额外空间。
*/
86. 分隔链表 - 力扣(LeetCode)
给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
你应当 保留 两个分区中每个节点的初始相对位置。
func partition(_ head: ListNode?, _ x: Int) -> ListNode? {
// 引入dummy节点
let pervDummy = ListNode(0), postDummy = ListNode(0)
var prev = pervDummy, post = postDummy
var node = head
// 使用尾插法处理左边和右边
while node != nil {
if node!.val < x {
prev.next = node
prev = node!
} else {
post.next = node
post = node!
}
node = node!.next
}
// 防止构成环
post.next = nil
// 左右拼接
prev.next = postDummy.next
return pervDummy.next
}
/*
* 快行指针(两个指针相差2倍速)
* 时间复杂度: O(n),其中 n 是原链表的长度。我们对该链表进行了一次遍历。
* 空间复杂度:O(1)。
*/
19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
func removeNthFromEnd(_ head: ListNode?, _ n: Int) -> ListNode? {
guard let head = head else {
return nil
}
let dummy = ListNode(0)
dummy.next = head
var prev: ListNode? = dummy
var post: ListNode? = dummy
// 设置后一个节点的初始位置
for _ in 0..<n {
if post == nil {
break
}
post = post!.next
}
// 同时移动前后节点
while post != nil && post!.next != nil {
prev = prev!.next
post = post!.next
}
// 删除节点
prev!.next = prev!.next!.next
return dummy.next
}
/*
* 快行指针(两个指针相差n个节点)
* 时间复杂度: O(n),其中 n 是原链表的长度。
* 空间复杂度:O(1)。
*/
栈和队列
71. 简化路径 - 力扣(LeetCode)
func simplifyPath(_ path: String) -> String {
// 用数组实现栈的功能
var pathStack = [String]()
// 拆分原路径
let paths = path.components(separatedBy: "/")
for path in paths {
// 对于"."直接跳过
guard path != "." else {
continue
}
// 对于 ".." 使用 pop 操作
if path == ".." {
if (pathStack.count > 0) {
pathStack.removeLast()
}
// 对于空数组的特殊情况
} else if path != "" {
pathStack.append(path)
}
}
// 将栈中的内容转化为优化后的路径
let res = pathStack.reduce("") { total, dir in "\(total)/\(dir)" }
// 注意空路径的结果是 "/"
return res.isEmpty ? "/" : res
}
/*
* 栈
* 时间复杂度:O(n),其中 n 是字符串 path 的长度。
* 空间复杂度:O(n)。我们需要 O(n) 的空间存储 paths 中的所有字符串。
*/
二叉树
// ----- 递归方式
//前序遍历 根节点 -> 左节点 -> 右节点
func preOrder(_ root: TreeNode?) -> Void {
guard root != nil else { return }
print("\(root!.val)\t", terminator: "")
preOrder(root!.left)
preOrder(root!.right)
}
//中序遍历 左节点 -> 根节点 -> 右节点
func midOrder(_ root: TreeNode?) -> Void {
guard root != nil else { return }
midOrder(root!.left)
print("\(root!.val)\t", terminator: "")
midOrder(root!.right)
}
//后序遍历 左节点 -> 右节点 -> 根节点
func afterOrder(_ root: TreeNode?) -> Void {
guard root != nil else { return }
afterOrder(root!.left)
afterOrder(root!.right)
print("\(root!.val)\t", terminator: "")
}
104. 二叉树的最大深度 - 力扣(LeetCode)
给定一个二叉树,找出其最大深度。 二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。 说明: 叶子节点是指没有子节点的节点。
func maxDepth(_ root: TreeNode?) -> Int {
guard let root = root else { return 0 }
return max(maxDepth(root.left), maxDepth(root.right)) + 1
}
/*
* 递归(深度优先搜索)
* 时间复杂度:O(n),n为二叉树节点的个数。每个节点在递归中只被遍历一次。
* 空间复杂度:O(height),其中height表示二叉树的高度。递归函数需要栈空间,而栈空间取决于递归的深度,因此空间复杂度等价于二叉树的高度。
*/
98. 验证二叉搜索树 - 力扣(LeetCode)
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左子树只包含 小于 当前节点的数。
- 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
func isValidBST(_ root: TreeNode?) -> Bool {
return _help(root, Int.min, Int.max)
}
func _help(_ node: TreeNode?, _ low: Int, _ upper: Int) -> Bool {
guard let node = node else { return true }
// 所有右子树节点的值都必须大于根节点的值
if low != Int.min && node.val <= low {
return false
}
// 所有左子树节点的值都必须小于根节点的值
if upper != Int.max && node.val >= upper {
return false
}
return _help(node.left, low, node.val) && _help(node.right, node.val, upper)
}
/*
* 递归
* 时间复杂度:O(n),其中 n 为二叉树的节点个数。在递归调用的时候二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n)。
* 空间复杂度:O(n),其中 n 为二叉树的节点个数。递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,即二叉树的高度。最坏情况下二叉树为一条链,树的高度为 n ,递归最深达到 n 层,故最坏情况下空间复杂度为 O(n) 。
*/
二叉树遍历
144. 二叉树的前序遍历 - 力扣(LeetCode)
// 抄自书
func preorderTraversal(_ root: TreeNode?) -> [Int] {
var res = [Int]()
var stack = [TreeNode]()
var node = root
while !stack.isEmpty || node != nil {
if node != nil {
res.append(node!.val)
stack.append(node!)
node = node!.left
} else {
node = stack.removeLast().right
}
}
return res;
}
// 力扣作者:huang-ye-v
func preorderTraversal(_ root: TreeNode?) -> [Int] {
guard root != nil else { return [] }//空拦截
var preOrderAry = [Int]()
var stack = [TreeNode]()//辅助栈
var cur = root
while stack.isEmpty == false || cur != nil {
while cur != nil {//左子树探底
stack.append(cur!)
preOrderAry.append(cur!.val)
cur = cur!.left
}
let tmp = stack.popLast()!//栈顶元素出栈
cur = tmp.right// 转到右子树继续循环
}
return preOrderAry
}
/*
* 迭代「DFS」
* 时间复杂度:O(n),其中 n 是二叉树的节点数。每一个节点恰好被遍历一次。
* 空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。
*/
94. 二叉树的中序遍历 - 力扣(LeetCode)
// 111
func inorderTraversal(_ root: TreeNode?) -> [Int] {
var res = [Int]()
var stack = [TreeNode]()
var node = root
while !stack.isEmpty || node != nil {
if node != nil {
stack.append(node!)
node = node!.left
} else {
node = stack.removeLast()
res.append(node!.val)
node = node!.right
}
}
return res;
}
// 力扣作者:huang-ye-v
func inorderTraversal(_ root: TreeNode?) -> [Int] {
guard root != nil else { return []}
var inorderAry = [Int]()
var stack = [TreeNode]()
var cur = root
while stack.isEmpty == false || cur != nil {
while cur != nil {
stack.append(cur!)
cur = cur!.left
}
let tmp = stack.popLast()!
inorderAry.append(tmp.val)//访问节点的操作放在出栈的时候
cur = tmp.right
}
return inorderAry
}
/*
* 迭代「DFS」
* 时间复杂度:O(n),其中 n 为二叉树节点的个数。二叉树的遍历中每个节点会被访问一次且只会被访问一次。
* 空间复杂度:O(n)。空间复杂度取决于栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n) 的级别。
*/
145. 二叉树的后序遍历 - 力扣(LeetCode)
func postorderTraversal(_ root: TreeNode?) -> [Int] {
var res = [Int]()
var stack = [TreeNode]()
var node = root
var prev: TreeNode? = nil
while !stack.isEmpty || node != nil {
while node != nil {
stack.append(node!)
node = node!.left
}
node = stack.removeLast()
if node!.right == nil || node!.right === prev {
res.append(node!.val)
prev = node!
node = nil
} else {
stack.append(node!)
node = node!.right
}
}
return res;
}
// 力扣作者:huang-ye-v
func postorderTraversal(_ root: TreeNode?) -> [Int] {
guard root != nil else { return []}
var postorderAry = [Int]()
var stack = [TreeNode]()
var cur = root
while stack.isEmpty == false || cur != nil {
while cur != nil {
stack.append(cur!)
postorderAry.append(cur!.val)
cur = cur!.right
}
let tmp = stack.popLast()!
cur = tmp.left
}
return postorderAry.reversed()
}
102. 二叉树的层序遍历 - 力扣(LeetCode)
// 代码随想
func levelOrder(_ root: TreeNode?) -> [[Int]] {
var result = [[Int]]()
guard let root = root else { return result }
// 表示一层
var queue = [root]
while !queue.isEmpty {
let count = queue.count
var subarray = [Int]()
for _ in 0 ..< count {
// 当前层
let node = queue.removeFirst()
subarray.append(node.val)
// 下一层
if let node = node.left { queue.append(node) }
if let node = node.right { queue.append(node) }
}
result.append(subarray)
}
return result
}
// 作者:huang-ye-v
func BFSOrder(root:TreeNode?) -> [Int] {
guard root != nil else { return [] }
var queue:[TreeNode] = [root!]// 队列辅助,根节点入队
var rst = [Int]()
while queue.isEmpty == false {
let node = queue.removeFirst()
rst.append(node.val)
if node.left != nil {
queue.append(node.left!)
}
if node.right != nil {
queue.append(node.right!)
}
}
return rst
}
/* 广度优先搜索「BFS」
* 时间复杂度:每个点进队出队各一次,故渐进时间复杂度为 O(n)。
* 空间复杂度:队列中元素的个数不超过 n 个,故渐进空间复杂度为 O(n)。
*/
排序和搜索
剑指 Offer II 074. 合并区间 - 力扣(LeetCode)
// 合并区间 作者:shen-jing-wa-45n 时间O(nlogn) 空间O(1)
func merge(_ intervals: [[Int]]) -> [[Int]] {
// 先排序 按照 首位小的在前
let intervals = intervals.sorted { $0[0] < $1[0] }
// 存放结果,先把 0 位放进去
var ans: [[Int]] = [intervals[0]]
// 从 1 位开始和结果比较
for j in 1 ..< intervals.count {
// 待处理数据
var row = intervals[j]
for i in 0 ..< ans.count {
// 已有结果
var list = ans[i]
// 待处理最小值 在 已有结果 区间内部,则有重叠
if row[0] <= list[1] {
// 因为一开始已经排序了,只需更新最大区间
list[1] = max(list[1], row[1])
ans[i] = list
// 清空当前待处理数据
row = []
break
}
}
// 当前待处理数据不在区间内 加入结果
if !row.isEmpty {
ans.append(row)
}
}
return ans
}
let intervals = [[1,3],[2,6],[8,10],[15,18]]
print(merge(intervals))
/*
* 时间复杂度:O(nlogn),其中 n 为区间的数量。除去排序的开销,
* 我们只需要一次线性扫描,所以主要的时间开销是排序的 O(nlogn)。
* 空间复杂度:O(logn),其中 n 为区间的数量。这里计算的是存储答案之外,
* 使用的额外空间。O(logn) 即为排序所需要的空间复杂度。
*/
704. 二分查找 - 力扣(LeetCode)
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
// 代码随想
// (版本一)左闭右闭区间
func search1(_ nums: [Int], _ target: Int) -> Int {
// 1. 先定义区间。这里的区间是[left, right]
var left = 0
var right = nums.count - 1
while left <= right {// 因为taeget是在[left, right]中,包括两个边界值,所以这里的left == right是有意义的
// 2. 计算区间中间的下标(如果left、right都比较大的情况下,left + right就有可能会溢出)
// let middle = (left + right) / 2
// 防溢出:
let middle = left + (right - left) / 2
// 3. 判断
if target < nums[middle] {
// 当目标在区间左侧,就需要更新右边的边界值,新区间为[left, middle - 1]
right = middle - 1
} else if target > nums[middle] {
// 当目标在区间右侧,就需要更新左边的边界值,新区间为[middle + 1, right]
left = middle + 1
} else {
// 当目标就是在中间,则返回中间值的下标
return middle
}
}
// 如果找不到目标,则返回-1
return -1
}
/*
* 时间复杂度:O(logn),其中 n 是数组的长度。
* 空间复杂度:O(1)
*/
// (版本二)左闭右开区间
func search2(_ nums: [Int], _ target: Int) -> Int {
var left = 0
var right = nums.count
while left < right {
let middle = left + ((right - left) >> 1)
if target < nums[middle] {
right = middle
} else if target > nums[middle] {
left = middle + 1
} else {
return middle
}
}
return -1
}
278. 第一个错误的版本 题解 - 力扣(LeetCode)
假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
func firstBadVersion(_ n: Int) -> Int {
// 处理特殊情况
guard n >= 1 else { return -1 }
var left = 1, right = n, mid = 0
while left < right {
mid = (right - left) / 2 + left
if isBadVersion(mid) {
right = mid
} else {
left = mid + 1
}
}
return left // return right 同样正确
}
/*
* 时间复杂度:O(logn),其中 n 是给定版本的数量。
* 空间复杂度:O(1)。我们只需要常数的空间保存若干变量。
*/
33. 搜索旋转排序数组 - 力扣(LeetCode)
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
func search(_ nums: [Int], _ target: Int) -> Int {
var (left, mid, right) = (0, 0, nums.count - 1)
while left <= right {
mid = (right - left) / 2 + left
if nums[mid] == target {
return mid
}
// 旋转位置特别靠前
if nums[mid] >= nums[left] {
if nums[mid] > target && target >= nums[left] {
right = mid - 1
} else {
left = mid + 1
}
} else {// 旋转位置特别靠后
if nums[mid] < target && target <= nums[right] {
left = mid + 1
} else {
right = mid - 1
}
}
}
return -1
}
/*
* 时间复杂度:O(logn),其中 n 为 nums 数组的大小。整个算法时间复杂度即为二分查找的时间复杂度 O(logn)。
* 空间复杂度:O(1)。我们只需要常数级别的空间存放变量。
*/
35. 搜索插入位置 - 力扣(LeetCode)
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
// 作者-代码随想
// 暴力法
func searchInsert1(_ nums: [Int], _ target: Int) -> Int {
for i in 0..<nums.count {
if nums[i] >= target {
return i
}
}
return nums.count
}
// 二分法
func searchInsert(_ nums: [Int], _ target: Int) -> Int {
var left = 0
var right = nums.count - 1
while left <= right {
let middle = left + ((right - left) >> 1)
if nums[middle] > target {
right = middle - 1
}else if nums[middle] < target {
left = middle + 1
}else if nums[middle] == target {
return middle
}
}
return right + 1
}
/*
* 时间复杂度:O(logn),其中 n 为数组的长度。二分查找所需的时间复杂度为 O(logn)。
* 空间复杂度:O(1)。只需要常数的空间保存若干变量。
*/
深度优先搜索和广度优先搜索
542. 01 矩阵 - 力扣(LeetCode)
给定一个由 0 和 1 组成的矩阵 mat ,请输出一个大小相同的矩阵,其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。
func updateMatrix(_ mat: [[Int]]) -> [[Int]] {
let m = mat.count
let n = mat[0].count
var ans = Array(repeating: Array(repeating: 0, count: n), count: m)
var visited = Array(repeating: Array(repeating: false, count: n), count: m)
var queue: ArraySlice<(Int, Int)> = []
for i in 0 ..< m {
for j in 0 ..< n where mat[i][j] == 0 {
queue.append((i, j))
visited[i][j] = true
}
}
while !queue.isEmpty {
let pos = queue.removeFirst()
let next = [(pos.0 + 1, pos.1), (pos.0 - 1, pos.1), (pos.0, pos.1 + 1), (pos.0, pos.1 - 1)]
for (i, j) in next {
if (0 ..< m).contains(i), (0 ..< n).contains(j), !visited[i][j] {
queue.append((i, j))
visited[i][j] = true
ans[i][j] = ans[pos.0][pos.1] + 1
}
}
}
return ans
}
/*
* 广度优先搜索
* 时间复杂度:O(mn),其中 m 为矩阵行数,n 为矩阵列数,即矩阵元素个数。
* 广度优先搜索中每个位置最多只会被加入队列一次,因此只需要 O(mn) 的时间复杂度。
* 空间复杂度:O(mn),其中 m 为矩阵行数,n 为矩阵列数,即矩阵元素个数。
* 除答案数组外,最坏情况下矩阵里所有元素都为 0,全部被加入队列中,此时需要 O(mn) 的空间复杂度。
*/
79. 单词搜索 - 力扣(LeetCode)
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
func exist(_ board: [[Character]], _ word: String) -> Bool {
guard board.count > 0 && board[0].count > 0 else {
return false
}
let (m, n) = (board.count, board[0].count)
var visited = Array(repeating: Array(repeating: false, count: n), count: m)
let wordContent = [Character](word)
for i in 0 ..< m {
for j in 0 ..< n {
if dfs(board, wordContent, m, n, i, j, &visited, 0) {
return true
}
}
}
return false
}
func dfs(_ board: [[Character]], _ wordContent: [Character], _ m: Int, _ n: Int, _ i: Int, _ j: Int, _ visited: inout [[Bool]], _ index: Int) -> Bool{
if index == wordContent.count {
return true
}
guard i >= 0 && i < m && j >= 0 && j < n else {
return false
}
guard !visited[i][j] && board[i][j] == wordContent[index] else {
return false
}
visited[i][j] = true
if dfs(board, wordContent, m, n, i + 1, j, &visited, index + 1) || dfs(board, wordContent, m, n, i - 1, j, &visited, index + 1) || dfs(board, wordContent, m, n, i, j + 1, &visited, index + 1) || dfs(board, wordContent, m, n, i, j - 1, &visited, index + 1) {
return true
}
visited[i][j] = false
return false
}
/*
* 深度优先搜索 + 回溯
* 时间复杂度:一个非常宽松的上界为 O(MN⋅3 L),其中 M,N 为网格的长度与宽度,L 为字符串 word 的长度。
* 在每次调用函数 check 时,除了第一次可以进入 4 个分支以外,其余时间我们最多会进入 3 个分支(因为每个位置只能使用一次,所以走过来的分支没法走回去)。
* 由于单词长为 L,故 check(i,j,0) 的时间复杂度为 O(3 L),而我们要执行 O(MN) 次检查。
* 然而,由于剪枝的存在,我们在遇到不匹配或已访问的字符时会提前退出,终止递归流程。
* 因此,实际的时间复杂度会远远小于 Θ(MN⋅3 L)。
* 空间复杂度:O(MN)。我们额外开辟了 O(MN) 的 visited 数组,同时栈的深度最大为 O(min(L,MN))。
*/
动态规划
72. 编辑距离 - 力扣(LeetCode)
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
func minDistance(_ word1: String, _ word2: String) -> Int {
if word1.count == 0 {return word2.count}
if word2.count == 0 {return word1.count}
var dp:[Int] = Array(repeating: 0, count: word2.count + 1)
let ary1 = Array(word1)
let ary2 = Array(word2)
for j in 0...ary2.count{
dp[j] = j
}
for i in 1...ary1.count{
var cur = i - 1
dp[0] = i - 1
for j in 1...ary2.count{
var leftTop = cur
cur = dp[j]
let left = dp[j - 1] + 1
let top = dp[j] + 1
if ary1[i - 1] != ary2[j - 1] {
leftTop = leftTop + 1
}
dp[j] = min(min(left, top), leftTop)
}
}
return dp[word2.count]
}
/*
* 动态规划
* 时间复杂度:O(mn),其中 m 为 word1 的长度,n 为 word2 的长度。
* 空间复杂度:O(mn),我们需要大小为 O(mn) 的 D 数组来记录状态值。
*/