Leetcode Interview Preparation Notes #
Basic Data Structures #
Arrays #
In Python, arrays are typically represented using lists. While Python doesn’t have a native array type as seen in other languages like Java or C++, lists are versatile and can be used similarly to arrays.
【Last Update: 2024-08-14
】
arr = [] # O(1)
arr = [1, 2, 3] # O(n), where n is the number of elements
first_element = arr[0] # O(1)
arr[1] = 10 # O(1)
arr.append(6) # O(1) on average for appending
arr.insert(2, 15) # O(n), where n is the number of elements after the insertion index
arr.remove(15) # O(n), where n is the number of elements in the list [remove the first 15 in the array]
del arr[2] # O(n), where n is the number of elements after the deleted index
last_element = arr.pop() # O(1)
arr.sort() # 原地排序
sorted_arr = sorted(arr) # 返回排序后的数组
arr[::-1] # arr 倒序
## Counter() 的常用语法和使用情况
from collections import Counter
arr = [1, 2, 2, 3, 3, 3]
counts = Counter(arr) # 结果:Counter({3: 3, 2: 2, 1: 1})
## 找到出现次数最多的元素
most_common_element = counts.most_common(1)[0] # 结果:(3, 3)
## 判断出现的元素是否相同
arr1 = [1, 2, 3]
arr2 = [3, 2, 1]
is_anagram = Counter(arr1) == Counter(arr2) # 结果:True
## set() 的常用语法和使用情况
arr = [1, 2, 2, 3, 4, 4]
## 快速查找
seen = set(arr)
if 3 in seen:
print("3 is in array")
## 去重
unique_elements = list(set(arr)) # 结果:[1, 2, 3, 4]
## 两个数组的交集
arr1 = [1, 2, 2, 3]
arr2 = [2, 3, 4]
intersection = list(set(arr1) & set(arr2)) # 结果:[2, 3]
Strings #
Strings in Python are immutable sequences of characters. You can perform various operations on strings using built-in methods and operators.
【Last Update: 2024-08-14
】
s = "Hello, World!" # O(n), where n is the length of the string
first_char = s[0] # O(1)
substring = s[7:12] # O(k), where k is the length of the substring
combined = s + " Python" # O(n + m), where n and m are the lengths of the two strings
repeated = s * 2 # O(n * k), where k is the number of repetitions
upper_s = s.upper() # O(n), where n is the length of the string
lower_s = s.lower() # O(n), where n is the length of the string
starts_with_hello = s.startswith("Hello") # O(n), where n is the length of the prefix
contains_world = "World" in s # O(n * m), where n is the length of the string and m is the length of the substring
replaced_s = s.replace("World", "Python") # O(n * m), where n is the length of the string and m is the length of the substring
words = s.split(", ") # O(n), where n is the length of the string
joined = " - ".join(words) # O(n), where n is the total length of the resulting string
string = "Hello, World!"
reversed_string = string[::-1] # 使用切片语法将字符串的顺序反过来 -> !dlroW ,olleH
Linked Lists #
A Linked List is a linear data structure consisting of nodes, where each node contains:
- A data part that stores the actual data.
- A next part (or pointer) that points to the next node in the list.
【Last Update: 2024-11-14
】
## A node in a linked list can be represented as a class
class ListNode:
def __init__(self, data=0, next=None):
self.data = data # Data of the node
self.next = next # Pointer to the next node
## Inserting Nodes
def insert_at_beginning(head, data):
new_node = ListNode(data) # Create a new node
new_node.next = head # Link the new node to the current head
return new_node # New node becomes the head
## Deleting Nodes
def delete_from_beginning(head):
if not head:
return None
return head.next # The second node becomes the new head
## Searching for a Node
def search(head, key):
current = head
while current:
if current.data == key:
return True # Found the data
current = current.next
return False # Data not found
Stack #
A Stack is a linear data structure that stores items in a Last-In/First-Out (LIFO) or First-In/Last-Out (FILO) manner. In stack, a new element is added at one end and an element is removed from that end only.
push(a)
– Inserts the element ‘a’ at the top of the stack – Time Complexity: O(1)pop()
– Deletes the topmost element of the stack – Time Complexity: O(1)Peek
- View the top element without removing it.Empty
- Check if the stack is empty.
stack = []
# Push elements onto the stack
stack.append(1)
stack.append(2)
# Pop element from the stack
top = stack.pop() # Removes and returns 2
# Peek the top element
top = stack[-1] if stack else None # Returns 1
# Check if the stack is empty
is_empty = len(stack) == 0
Monotonic Stack (单调堆栈) #
A monotonic stack is a specialized data structure used to solve problems involving arrays or sequences, particularly where you need to efficiently find next greater/smaller elements or previous greater/smaller elements. The stack is maintained in either increasing or decreasing order based on the problem requirements.
- 使用堆栈存储数组的 indices 或 value。
- 通过在处理新元素时 popping 违反顺序的元素来保持单调性。
- 通常迭代数组一次(从左到右或从右到左)以实现所需的结果。
【Last Update: 2024-12-10
】
Queue #
Queue is a linear data structure that stores items in First In First Out (FIFO) manner. With a queue the least recently added item is removed first.
Enqueue
: Adds an item to the queue. If the queue is full, then it is said to be an Overflow condition – Time Complexity : O(1)Dequeue
: Removes an item from the queue. The items are popped in the same order in which they are pushed. If the queue is empty, then it is said to be an Underflow condition – Time Complexity : O(1)Peek
: View the front element without removing it.Empty
: Check if the queue is empty.
【Last Update: 2024-11-19
】
from collections import deque
# Initialize a queue
queue = deque()
# Enqueue elements
queue.append(1)
queue.append(2)
# Dequeue element
front = queue.popleft() # Removes and returns 1
# Peek at the front element
front = queue[0] if queue else None
# Check if the queue is empty
is_empty = len(queue) == 0
Deque #
A deque is a generalized queue that allows insertion and deletion from both ends with O(1) complexity. Internally, it is implemented as a doubly linked list or a circular buffer.
【Last Update: 2024-11-25
】
from collections import deque
# Initialize a deque
dq = deque()
# Add elements
dq.append(1) # Add to the right
dq.appendleft(2) # Add to the left
# Remove elements
dq.pop() # Remove from the right
dq.popleft() # Remove from the left
# Access and manipulation
dq.extend([3, 4]) # Add multiple elements to the right
dq.extendleft([0, -1]) # Add multiple elements to the left (reversed order)
dq.rotate(1) # Rotate elements right
dq.rotate(-1) # Rotate elements left
dq.clear() # Clear all elements
Advanced Data Structures #
Heap #
A heap is a complete binary tree stored as an array. It maintains the heap property: in a min-heap, the parent is less than or equal to its children. Insertions and deletions are O(log n) due to the need to maintain the heap property.
- Two main types:
- Min-Heap: The root node is the smallest, and every parent node is smaller than or equal to its children.
- Max-Heap: The root node is the largest, and every parent node is larger than or equal to its children.
- Root Node Access:
- Min-Heap: Root is the smallest element
- Max-Heap: Root is the largest element.
- Efficient Operations:
- Insert and delete both take O(log n).
- Maintains heap properties using adjustments (upward or downward shifts).
【Last Update: 2024-11-25
】
import heapq
# Initialize a heap
heap = []
# Add elements
heapq.heappush(heap, 3) # Push element into the heap
heapq.heappush(heap, 1)
heapq.heappush(heap, 4)
# Access the smallest element
smallest = heap[0]
# Remove elements
min_element = heapq.heappop(heap) # Pop the smallest element
# Heapify an existing list
nums = [4, 1, 7, 3]
heapq.heapify(nums)
# Get n largest or smallest elements
largest = heapq.nlargest(2, nums)
smallest = heapq.nsmallest(2, nums)
Hash Tables #
In Python, the built-in dict type (short for dictionary) functions as a hash table. Hash tables are a key data structure used for efficient data retrieval and storage, providing average time complexities of O(1) for insertion, deletion, and lookup operations due to their underlying hashing mechanism.
【Last Update: 2024-11-06
】
my_dict = {} # Creating an empty dictionary
my_dict = {'key1': 'value1', 'key2': 'value2'} # Creating a dictionary with initial values
value = my_dict['key1'] # Accessing a value by key
my_dict['key3'] = 'value3' # Adding a new key-value pair
my_dict['key2'] = 'new_value2' # Updating an existing key-value pair
del my_dict['key1'] # Removing an entry by key
value = my_dict.pop('key2') # Popping an entry (removes and returns the value)
exists = 'key3' in my_dict # # Checking if a key is in the dictionary [True]
for key in my_dict:
print(key, my_dict[key]) # Iterating through keys
for key, value in my_dict.items(): # Iterating through key-value pairs
print(key, value)
for value in my_dict.values(): # Iterating through values
print(value)
# defaultdict 使用方法,没见过的元素不会报错。适用于计数、分组和嵌套字典等应用。
from collections import defaultdict
# 使用 int 类型的 defaultdict
dd = defaultdict(int)
print(dd['missing_key']) # 输出:0,因为 int() 的默认值是 0
print(dd) # 输出:defaultdict(<class 'int'>, {'missing_key': 0})
# 统计元素出现次数
data = "abracadabra"
counter = defaultdict(int)
for char in data:
counter[char] += 1
print(counter) # 输出:defaultdict(<class 'int'>, {'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
# defaultdict(list)常用于将多个值归类到同一个键下。
data = [("apple", 1), ("banana", 2), ("apple", 3), ("banana", 4)]
grouped_data = defaultdict(list)
for fruit, count in data:
grouped_data[fruit].append(count)
print(grouped_data) # 输出:defaultdict(<class 'list'>, {'apple': [1, 3], 'banana': [2, 4]})
# 可以使用dict()将defaultdict转换为普通字典。
dd = defaultdict(int)
dd['a'] += 1
print(dict(dd)) # 输出:{'a': 1}
Tree #
A tree is a hierarchical data structure with nodes connected by edges. The topmost node is the root, and nodes with no children are called leaves.
- Binary Tree: Each node has at most two children.
- Binary Search Tree (BST): A binary tree where the left child contains values less than the parent, and the right child contains values greater.
- Balanced Tree: A tree where the height difference between left and right subtrees of any node is minimal (e.g., AVL tree, Red-Black tree).
- Tree Traversals:
- Preorder Traversal (Root, Left, Right)
- Inorder Traversal (Left, Root, Right)
- Postorder Traversal (Left, Right, Root)
## Trees are often represented using classes.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
## Preorder Traversal (Root, Left, Right)
def preorder_traversal(root):
if root:
print(root.val)
preorder_traversal(root.left)
preorder_traversal(root.right)
## Inorder Traversal (Left, Root, Right)
def inorder_traversal(root):
if root:
inorder_traversal(root.left)
print(root.val)
inorder_traversal(root.right)
## Postorder Traversal (Left, Right, Root)
def postorder_traversal(root):
if root:
postorder_traversal(root.left)
postorder_traversal(root.right)
print(root.val)
## Binary Search Tree (BST) Operations
## 1. Insert a Node
def insert_into_bst(root, val):
if not root:
return TreeNode(val)
if val < root.val:
root.left = insert_into_bst(root.left, val)
else:
root.right = insert_into_bst(root.right, val)
return root
## 2. Search for a Value
def search_bst(root, val):
if not root or root.val == val:
return root
if val < root.val:
return search_bst(root.left, val)
return search_bst(root.right, val)
## 3. Delete a Node
def delete_node(root, key):
if not root:
return None
if key < root.val:
root.left = delete_node(root.left, key)
elif key > root.val:
root.right = delete_node(root.right, key)
else:
if not root.left:
return root.right
if not root.right:
return root.left
min_larger_node = root.right
while min_larger_node.left:
min_larger_node = min_larger_node.left
root.val = min_larger_node.val
root.right = delete_node(root.right, root.val)
return root
Core Algorithms #
Overview #
- Two Pointer: The two-pointer technique is used primarily in solving array and linked list problems. It involves using two pointers to traverse the data structure, allowing for efficient searching and processing of elements.
- Sorting Algorithms: Review the mechanisms and use cases for quicksort, mergesort, and heapsort. Understand the trade-offs in terms of time and space complexity.
- Search Algorithms: Study binary search on sorted arrays, and learn about its variations for finding the first or last position of an element.
- Recursion and Backtracking: Understand how to apply recursion for solving problems involving permutations, combinations, and other backtrack-required scenarios. Study the call stack mechanism and how to optimize recursion through memoization.
- Prefix Sum and Suffix Sum: Prefix Sum and Suffix Sum are techniques used to compute the sum of elements in a subarray quickly by precomputing cumulative sums.
Two Pointer #
- Finding Pairs with a Given Sum: When looking for two numbers in a sorted array that add up to a specific target.
- Reversing a String or Array: Using two pointers to swap elements from the start and end until they meet in the middle.
- Merging Two Sorted Arrays: Traversing both arrays simultaneously to create a new sorted array.
- Removing Duplicates from a Sorted Array: Using two pointers to track unique elements.
- 设置 two pointers 的时候,left 一般会在最前面,但是 right 不一定在最后,可以设置在 left 后面。
【Last Update: 2024-11-07
】
Prefix Sum and Suffix Sum #
- Prefix Sum: For an array nums, the prefix sum at each index i is the sum of all elements from the start of the array up to i. This allows you to find the sum of any subarray [i, j] in constant time by calculating prefix[j+1] - prefix[i].
- Suffix Sum: For the same array nums, the suffix sum at index i is the sum of all elements from i to the end of the array. It enables efficient queries for sums of subarrays that start from any index i to a given end by using suffix[i] - suffix[j+1].
【Last Update: 2024-11-11
】
## Input [1, 2, 3, 4] -> Output [2x3x4, 1x3x4, 1x2x4, 1x2x3] = [24, 12, 8, 6]
## Predix -> [0, 1, 1x2, 1x2x3] = [0, 1, 2, 6]
## Suffix -> [2x3x4, 3x4, 4, 0] = [24, 12, 4, 0]
def productExceptSelf(self, nums: List[int]) -> List[int]:
res = [1] * len(nums)
prefix, suffix = 1, 1
for i in range(len(nums)):
res[i] = prefix
prefix *= nums[i]
for j in range(len(nums)-1,-1,-1):
res[j] *= suffix
suffix *= nums[j]
return res
Binary Search #
二分查找(Binary Search)是一种高效的查找算法,主要用于在有序数组或其他有序结构中快速定位目标值。其核心思想是每次将搜索范围缩小一半,直到找到目标值或搜索范围为空。
- 初始化左右指针
left
和right
,分别指向数组的起始和结束位置。 - 计算中间点索引
mid = left + (right - left) // 2
,避免直接(left + right) // 2
的溢出风险。 - 比较中间点值
arr[mid]
与目标值target
:- 如果
arr[mid] == target
,找到目标,返回mid
。 - 如果
arr[mid] < target
,目标值在右侧,调整left = mid + 1
。 - 如果
arr[mid] > target
,目标值在左侧,调整right = mid - 1
。
- 如果
- 循环继续,直到
left > right
。
【Last Update: 2024-12-18
】
- Iterative 版本
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] == target:
return mid # 找到目标
elif arr[mid] < target:
left = mid + 1 # 目标值在右侧
else:
right = mid - 1 # 目标值在左侧
return -1 # 未找到目标
- Recursive 版本
def binary_search_recursive(arr, target, left, right):
if left > right:
return -1
mid = left + (right - left) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
return binary_search_recursive(arr, target, mid + 1, right)
else:
return binary_search_recursive(arr, target, left, mid - 1)
Recursion #
递归(Recursion)中一个函数会直接或间接调用自身来解决问题。它通常用于将问题分解为子问题的形式。
- 问题结构: 问题可以分解为更小的子问题,且这些子问题具有相同的结构。
- 终止条件: 每个递归必须有基准情况(Base Case)以防止无限递归。
- 无“回溯”操作: 递归函数只关注每一层的子问题,不涉及撤销操作。
- 用途: 适用于分治问题(如二分搜索、归并排序)或递归定义的问题(如树的遍历)。
【Last Update: 2024-12-11
】
def fibonacci(n):
if n <= 1:
return n # 基准情况
return fibonacci(n-1) + fibonacci(n-2) # 递归关系
Backtracking #
回溯(Backtracking)是一种试探性的搜索算法,它通过递归探索所有可能的解,并在发现某条路径不满足条件时,撤销(回溯)当前的选择并尝试其他路径。
- 探索并撤销: 在探索某条路径时,如果发现不符合条件,会回退到上一步尝试其他可能。
- 约束条件: 通过加入剪枝(pruning)优化搜索,避免无意义的计算。
- 用途: 适用于需要生成所有解并验证解的正确性的问题。
【Last Update: 2024-12-11
】
def permutations(nums):
result = []
def backtrack(path, remaining):
if not remaining: # 终止条件:所有元素已被使用
result.append(path)
return
for i in range(len(remaining)):
backtrack(path + [remaining[i]], remaining[:i] + remaining[i+1:])
backtrack([], nums)
return result
Depth-First Search (DFS) #
DFS 是一种递归(Recursive)或使用栈(Stack)实现的算法。它会优先深入访问一个分支,直到该分支无法继续,再回溯(Backtrack)到上一层继续访问未探索的分支。更适合用于解决路径、连通性和循环检测等问题。
- 应用:
- 检测循环(Cycle Detection):通过递归栈检测图中是否存在循环。
- 连通分量(Connected Components):在无向图中找到所有连通分量。
- 路径搜索(Path Search):在迷宫(Maze)中找到从起点到终点的一条路径。
【Last Update: 2024-12-10
】
def dfs(graph, start, visited=None):
if visited is None:
visited = set()
visited.add(start)
for neighbor in graph[start]:
if neighbor not in visited:
dfs(graph, neighbor, visited)
return visited
graph = {0: [1, 2], 1: [0, 3], 2: [0, 4], 3: [1], 4: [2]}
print(dfs(graph, 0)) # Output: {0, 1, 2, 3, 4}
Breadth-First Search (BFS) #
BFS 是一种逐层遍历(Level-Order Traversal)的算法,优先访问距离起点较近的节点。它使用队列(Queue)实现,以确保节点按照发现的顺序访问。更适合解决最短路径(Shortest Path)等问题。
- 应用:
- 最短路径(Shortest Path):在无权图(Unweighted Graph)中计算两点之间的最短路径。
- 层级关系(Level Order Traversal):例如二叉树的层序遍历。
- 连通性检查(Connectivity Check):检查从某节点是否可以到达所有其他节点。
【Last Update: 2024-12-10
】
from collections import deque
def bfs_tree(root):
if not root:
return
queue = deque([root])
while queue:
node = queue.popleft()
print(node.val, end=" ") # 访问当前节点
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
bfs_tree(root) # 输出: 1 2 3 4 5
Advanced Algorithms #
Overview #
- Dynamic Programming: Explore the methodology of solving problems by breaking them down into smaller subproblems, storing results, and combining them to solve larger problems. Focus on understanding the concepts of overlapping subproblems and optimal substructure.
- Greedy Algorithms: Learn how greedy choices can lead to globally optimized solutions and their applications in problems like scheduling, graph based problems (like minimum spanning trees), and currency denomination.
- Graph Algorithms: Study shortest path algorithms (Dijkstra’s, Bellman-Ford) and minimum spanning tree algorithms (Prim’s, Kruskal’s). Understand their use cases and limitations.