位运算 & 数学 & 高频进阶九题通关(Python + C++)
位运算和数学技巧常常能以极低的复杂度解决问题;而堆、Trie、快速选择等高级数据结构则是面试中的加分项。本文整理9道经典题目,每道题包含:题目描述、解题思路、图解(文本示意)、Python代码、C++代码、复杂度分析。
📌 题目清单(位运算 & 数学)
| 题号 | 题目 | 核心考点 |
|---|---|---|
| 136 | 只出现一次的数字 | 异或(a^a=0) |
| 169 | 多数元素 | 摩尔投票法 / 排序 / 分治 |
| 7 | 整数反转 | 数学(溢出判断) |
| 50 | Pow(x, n) | 快速幂(递归 / 迭代) |
| 461 | 汉明距离 | 异或 + 位计数(x&(x-1)) |
📌 题目清单(其他高频)
| 题号 | 题目 | 核心考点 |
|---|---|---|
| 215 | 数组中的第 K 个最大元素 | 快速选择 / 堆 |
| 23 | 合并 K 个升序链表 | 优先队列 / 分治归并 |
| 208 | 实现 Trie (前缀树) | 字典树(插入/搜索/前缀) |
| 295 | 数据流中的中位数 | 两个堆(大根堆 + 小根堆) |
1. 只出现一次的数字(LeetCode 136)
题目描述
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现一次的元素。要求 O(n) 时间,O(1) 空间。
示例 :
输入:[2,2,1] → 输出:1
解题思路
- 利用异或性质:
a ^ a = 0,a ^ 0 = a,且异或满足交换律、结合律。 - 将所有数字异或起来,成对的数字抵消为 0,最终结果就是只出现一次的数字。
图解
数组 [2,2,1]
2 ^ 2 = 0
0 ^ 1 = 1
结果 1
Python代码
python
def singleNumber(nums):
res = 0
for num in nums:
res ^= num
return res
C++代码
cpp
class Solution {
public:
int singleNumber(vector<int>& nums) {
int res = 0;
for (int num : nums) res ^= num;
return res;
}
};
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2. 多数元素(LeetCode 169)
题目描述
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊n/2⌋ 的元素。假设数组非空且多数元素一定存在。
示例 :
输入:[3,2,3] → 输出:3
解题思路
- 摩尔投票法 :维护候选元素
candidate和计数count。遍历数组:- 若
count == 0,将当前元素设为候选。 - 若当前元素 == candidate,
count++,否则count--。 - 由于多数元素数量超过一半,最终
candidate即为答案。
- 若
图解
[3,2,3]
i=0: candidate=3, count=1
i=1: 2 != 3 → count=0
i=2: count=0 → candidate=3, count=1
结束 candidate=3
Python代码
python
def majorityElement(nums):
candidate = None
count = 0
for num in nums:
if count == 0:
candidate = num
count += 1 if num == candidate else -1
return candidate
C++代码
cpp
class Solution {
public:
int majorityElement(vector<int>& nums) {
int candidate = 0, count = 0;
for (int num : nums) {
if (count == 0) candidate = num;
count += (num == candidate) ? 1 : -1;
}
return candidate;
}
};
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
3. 整数反转(LeetCode 7)
题目描述
给你一个 32 位有符号整数 x,返回反转后的整数。如果反转后超出 32 位有符号整数范围 [−2³¹, 2³¹−1],则返回 0。
示例 :
输入:123 → 输出:321
输入:-123 → 输出:-321
解题思路
- 逐位取出:
pop = x % 10,x //= 10。 - 构建结果:
rev = rev * 10 + pop。 - 溢出判断:在加之前判断
rev > INT_MAX/10或rev < INT_MIN/10时返回 0。
图解
x = 123
rev=0, pop=3 → rev=3, x=12
pop=2 → rev=32, x=1
pop=1 → rev=321, x=0
返回 321
Python代码
python
def reverse(x):
INT_MAX = 2**31 - 1
INT_MIN = -2**31
rev = 0
sign = 1 if x >= 0 else -1
x = abs(x)
while x:
pop = x % 10
x //= 10
# 溢出检查
if rev > INT_MAX // 10 or (rev == INT_MAX // 10 and pop > 7):
return 0
rev = rev * 10 + pop
return sign * rev
C++代码
cpp
class Solution {
public:
int reverse(int x) {
int rev = 0;
while (x != 0) {
int pop = x % 10;
x /= 10;
if (rev > INT_MAX/10 || (rev == INT_MAX/10 && pop > 7)) return 0;
if (rev < INT_MIN/10 || (rev == INT_MIN/10 && pop < -8)) return 0;
rev = rev * 10 + pop;
}
return rev;
}
};
复杂度分析
- 时间复杂度:O(log₁₀ x)(位数)
- 空间复杂度:O(1)
4. Pow(x, n)(LeetCode 50)
题目描述
实现 pow(x, n),即计算 x 的整数 n 次幂。
示例 :
输入:x = 2.00000, n = 10 → 输出:1024.00000
解题思路
- 快速幂(递归/迭代) :
- 若
n为负数,转为1 / pow(x, -n)。 - 利用
x^n = (x^(n/2))^2,当 n 为奇数时再乘一个 x。
- 若
- 迭代写法:
result = 1,将 n 视为二进制,从低位向高位累乘。
图解(n=10,二进制 1010)
x=2, n=10
result=1, base=2
n=10 (1010): n%2=0 → base=4, n=5
n=5 (101): n%2=1 → result=1*4=4, base=16, n=2
n=2: n%2=0 → base=256, n=1
n=1: n%2=1 → result=4*256=1024, base=65536, n=0
返回 1024
Python代码(递归)
python
def myPow(x, n):
if n == 0:
return 1
if n < 0:
return 1 / myPow(x, -n)
half = myPow(x, n // 2)
if n % 2 == 0:
return half * half
else:
return half * half * x
C++代码(迭代)
cpp
class Solution {
public:
double myPow(double x, int n) {
double res = 1.0;
long long N = n; // 防止 -2147483648 取反溢出
if (N < 0) {
x = 1 / x;
N = -N;
}
while (N) {
if (N & 1) res *= x;
x *= x;
N >>= 1;
}
return res;
}
};
复杂度分析
- 时间复杂度:O(log n)
- 空间复杂度:O(log n)(递归) / O(1)(迭代)
5. 汉明距离(LeetCode 461)
题目描述
两个整数之间的汉明距离是指这两个数字对应二进制位不同的位置的数目。给出两个整数 x 和 y,计算汉明距离。
示例 :
输入:x = 1, y = 4 → 输出:2
(1: 001,4: 100,不同位数为2)
解题思路
- 先计算
xor = x ^ y,得到所有不同位为 1 的数字。 - 统计
xor中 1 的个数:可用while (xor) { count++; xor &= (xor - 1); }(每次消除最低位的1)。
图解
x=1 (001), y=4 (100)
xor = 101 (5)
count=0
xor=5 (101) → xor & (xor-1)=101 & 100=100, count=1
xor=4 (100) → 100 & 011=000, count=2
结束 count=2
Python代码
python
def hammingDistance(x, y):
xor = x ^ y
count = 0
while xor:
count += 1
xor &= xor - 1
return count
C++代码
cpp
class Solution {
public:
int hammingDistance(int x, int y) {
int xorVal = x ^ y;
int count = 0;
while (xorVal) {
count++;
xorVal &= xorVal - 1;
}
return count;
}
};
复杂度分析
- 时间复杂度:O(bit_count),最坏 O(32)
- 空间复杂度:O(1)
6. 数组中的第 K 个最大元素(LeetCode 215)
题目描述
给定整数数组 nums 和整数 k,返回数组中第 k 个最大的元素(即排序后的第 k 个最大元素,而非第 k 个不同元素)。
示例 :
输入:[3,2,1,5,6,4], k=2 → 输出:5
解题思路
- 快速选择(Quick Select):基于快排的 partition,每次将枢轴放到正确位置,若枢轴索引正好是第 k 大对应的下标(降序)则返回,否则只在一边递归。
- 堆:使用最小堆维护前 k 大的元素,堆顶即为答案。
图解(快速选择)
nums=[3,2,1,5,6,4], k=2(第2大即升序第5小,索引 len-k=4)
选枢轴4,partition后可能变成 [3,2,1,4,6,5],枢轴索引3 < 4,找右边
...
最终找到索引4的值5
Python代码(快速选择)
python
import random
def findKthLargest(nums, k):
def quick_select(l, r, k_smallest):
if l == r:
return nums[l]
pivot_idx = random.randint(l, r)
pivot_val = nums[pivot_idx]
nums[pivot_idx], nums[r] = nums[r], nums[pivot_idx]
store = l
for i in range(l, r):
if nums[i] < pivot_val:
nums[store], nums[i] = nums[i], nums[store]
store += 1
nums[store], nums[r] = nums[r], nums[store]
if k_smallest == store:
return nums[store]
elif k_smallest < store:
return quick_select(l, store - 1, k_smallest)
else:
return quick_select(store + 1, r, k_smallest)
n = len(nums)
return quick_select(0, n-1, n - k) # 第 k 大是升序第 n-k 小
C++代码(堆)
cpp
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> minHeap;
for (int num : nums) {
minHeap.push(num);
if (minHeap.size() > k) minHeap.pop();
}
return minHeap.top();
}
};
复杂度分析
- 快速选择:平均 O(n),最坏 O(n²);空间 O(log n)(递归栈)
- 堆:O(n log k);空间 O(k)
7. 合并 K 个升序链表(LeetCode 23)
题目描述
给你一个链表数组,每个链表都已按升序排列。请将所有链表合并成一个升序链表并返回。
示例 :
输入:[[1,4,5],[1,3,4],[2,6]] → 输出:[1,1,2,3,4,4,5,6]
解题思路
- 方法一:优先队列(小根堆):将每个链表的头节点放入堆中,每次弹出最小值节点,将其下一节点入堆。
- 方法二:分治归并:两两合并,重复直到只剩一个。
图解(优先队列)
lists: [1->4->5, 1->3->4, 2->6]
堆初始: (1,list0), (1,list1), (2,list2)
弹出1(list0) → result:1, 加入list0.next(4)
堆: (1,list1), (2,list2), (4,list0)
弹出1(list1) → result:1->1, 加入list1.next(3)
......
最终合并完成
Python代码(优先队列)
python
import heapq
def mergeKLists(lists):
heap = []
for i, node in enumerate(lists):
if node:
heapq.heappush(heap, (node.val, i, node))
dummy = ListNode()
cur = dummy
while heap:
val, i, node = heapq.heappop(heap)
cur.next = node
cur = cur.next
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
return dummy.next
C++代码(分治归并)
cpp
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
if (lists.empty()) return nullptr;
int step = 1;
while (step < lists.size()) {
for (int i = 0; i + step < lists.size(); i += 2*step) {
lists[i] = mergeTwo(lists[i], lists[i+step]);
}
step *= 2;
}
return lists[0];
}
ListNode* mergeTwo(ListNode* l1, ListNode* l2) {
ListNode dummy(0), *cur = &dummy;
while (l1 && l2) {
if (l1->val < l2->val) {
cur->next = l1; l1 = l1->next;
} else {
cur->next = l2; l2 = l2->next;
}
cur = cur->next;
}
cur->next = l1 ? l1 : l2;
return dummy.next;
}
};
复杂度分析
- 优先队列:O(N log k),其中 N 为总节点数,k 为链表数;空间 O(k)
- 分治归并:O(N log k),空间 O(1)(不计递归栈)
8. 实现 Trie (前缀树)(LeetCode 208)
题目描述
实现一个 Trie(前缀树),包含 insert、search、startsWith 三个操作。
示例:
python
trie = Trie()
trie.insert("apple")
trie.search("apple") # True
trie.search("app") # False
trie.startsWith("app") # True
trie.insert("app")
trie.search("app") # True
解题思路
- 每个节点包含一个子节点数组(26个字母)和一个布尔标志
isEnd。 insert:遍历单词的每个字符,若子节点不存在则创建,最后标记isEnd = True。search:遍历字符,若某字符缺失返回 False,最后检查isEnd。startsWith:类似但不检查isEnd。
图解
插入 "apple":
root -> a -> p -> p -> l -> e (isEnd)
插入 "app":
root -> a -> p -> p (isEnd)
Python代码
python
class TrieNode:
def __init__(self):
self.children = [None] * 26
self.isEnd = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
node = self.root
for ch in word:
idx = ord(ch) - ord('a')
if not node.children[idx]:
node.children[idx] = TrieNode()
node = node.children[idx]
node.isEnd = True
def search(self, word: str) -> bool:
node = self._find(word)
return node is not None and node.isEnd
def startsWith(self, prefix: str) -> bool:
return self._find(prefix) is not None
def _find(self, prefix):
node = self.root
for ch in prefix:
idx = ord(ch) - ord('a')
if not node.children[idx]:
return None
node = node.children[idx]
return node
C++代码
cpp
class Trie {
struct TrieNode {
TrieNode* children[26];
bool isEnd;
TrieNode() : isEnd(false) {
for (int i = 0; i < 26; ++i) children[i] = nullptr;
}
};
TrieNode* root;
public:
Trie() { root = new TrieNode(); }
void insert(string word) {
TrieNode* node = root;
for (char c : word) {
int idx = c - 'a';
if (!node->children[idx]) node->children[idx] = new TrieNode();
node = node->children[idx];
}
node->isEnd = true;
}
bool search(string word) {
TrieNode* node = find(word);
return node && node->isEnd;
}
bool startsWith(string prefix) {
return find(prefix) != nullptr;
}
private:
TrieNode* find(string& s) {
TrieNode* node = root;
for (char c : s) {
int idx = c - 'a';
if (!node->children[idx]) return nullptr;
node = node->children[idx];
}
return node;
}
};
复杂度分析
- 时间复杂度:插入/查询均为 O(L),L 为单词长度
- 空间复杂度:O(总字符数 × 26)
9. 数据流中的中位数(LeetCode 295)
题目描述
设计一个支持以下两种操作的数据结构:
addNum(num):从数据流中添加一个整数到数据结构中。findMedian():返回目前所有元素的中位数。
示例:
addNum(1), addNum(2), findMedian() → 1.5
addNum(3), findMedian() → 2
解题思路
- 使用两个堆:大根堆
left存储较小的一半(允许比 right 多一个元素),小根堆right存储较大的一半。 - 保证
len(left) >= len(right)且差值 ≤ 1。 - 添加元素时,先插入 left(取负),再平衡到 right,最后调整大小。
- 中位数:若两堆大小相等,取 (left.top + right.top)/2;否则取 left.top。
图解
add 1: left=[1], right=[] -> median=1
add 2: 先放left: left=[2,1], 弹出最大2放入right: left=[1], right=[2] median=(1+2)/2=1.5
add 3: 3>left.top(1), 放入right: right=[2,3]? 实际小根堆,[2,3] min=2,但需保证left大小>=right,将right最小移到left: left=[2,1], right=[3] median=2
Python代码
python
import heapq
class MedianFinder:
def __init__(self):
self.left = [] # 大根堆(存负数)
self.right = [] # 小根堆
def addNum(self, num: int) -> None:
heapq.heappush(self.left, -num)
heapq.heappush(self.right, -heapq.heappop(self.left))
if len(self.left) < len(self.right):
heapq.heappush(self.left, -heapq.heappop(self.right))
def findMedian(self) -> float:
if len(self.left) > len(self.right):
return -self.left[0]
else:
return (-self.left[0] + self.right[0]) / 2
C++代码
cpp
class MedianFinder {
priority_queue<int> left; // 大根堆
priority_queue<int, vector<int>, greater<int>> right; // 小根堆
public:
void addNum(int num) {
left.push(num);
right.push(left.top());
left.pop();
if (left.size() < right.size()) {
left.push(right.top());
right.pop();
}
}
double findMedian() {
if (left.size() > right.size()) return left.top();
else return (left.top() + right.top()) / 2.0;
}
};
复杂度分析
- 时间复杂度:addNum O(log n),findMedian O(1)
- 空间复杂度:O(n)
🎯 总结
| 题目 | 核心技巧 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 136. 只出现一次的数字 | 异或 | O(n) | O(1) |
| 169. 多数元素 | 摩尔投票法 | O(n) | O(1) |
| 7. 整数反转 | 数学 + 溢出判断 | O(log x) | O(1) |
| 50. Pow(x, n) | 快速幂 | O(log n) | O(1) |
| 461. 汉明距离 | 异或 + 位计数 | O(1) | O(1) |
| 215. 第K大元素 | 快速选择 / 堆 | O(n) 平均 / O(n log k) | O(log n) / O(k) |
| 23. 合并K个链表 | 优先队列 / 分治 | O(N log k) | O(k) / O(1) |
| 208. 前缀树 | 字典树 | O(L) | O(总字符数×26) |
| 295. 数据流中位数 | 两个堆 | add O(log n), find O(1) | O(n) |
位运算题需要熟悉异或、与、或、移位等基础;数学题注意溢出;堆和分治是处理 Top K、多路归并的利器;前缀树适合字符串匹配问题。多练习,形成肌肉记忆。