Solutions to high-frequency interview questions of LeetCode in C++17, taking into account both efficiency and comprehensibility.
O(log n)
,但插入为 O(n)
,总体时间复杂度 O(n ^ 2)
class Solution {
public:
vector<int> countSmaller(vector<int>& nums) {
int sz = size(nums);
vector<int> res(sz);
vector<int> v;
for (int i = sz - 1; i >= 0; --i) {
auto it = lower_bound(begin(v), end(v), nums[i]);
res[i] = it - begin(v);
v.emplace(it, nums[i]);
}
return res;
}
};
O(log n)
,总体时间复杂度 O(n log n)
class Solution {
public:
struct Node {
int val;
int cnt = 0;
int equal = 1;
Node* left = nullptr;
Node* right = nullptr;
Node(int v) : val(v) {}
};
vector<int> countSmaller(vector<int>& nums) {
if (empty(nums)) {
return {};
}
Node* root = new Node(nums.back());
vector<int> res(size(nums));
for (int i = size(nums) - 2; i >= 0; --i) {
res[i] = insert(root, nums[i]);
}
return res;
}
private:
int insert(Node* node, int val) {
if (val < node->val) {
++node->cnt;
if (!node->left) {
node->left = new Node(val);
return 0;
}
return insert(node->left, val);
}
if (val > node->val) {
if (!node->right) {
node->right = new Node(val);
return node->cnt + node->equal;
}
return node->cnt + node->equal + insert(node->right, val);
}
++node->equal;
return node->cnt;
}
};
lowbit
,它返回二进制数最后一个 1 的位置所表示的值int lowbit(int n) { return n & (~n + 1); }
int main() {
assert(lowbit(0b10001101) == 1);
assert(lowbit(0b11011010) == 2);
assert(lowbit(0b10101100) == 4);
assert(lowbit(0b10111000) == 8);
assert(lowbit(0b11010000) == 16);
}
lowbit
的原理如下xxxx xxxx xxxx 1000
按位取反 => yyyy yyyy yyyy 0111
取反加 1 => yyyy yyyy yyyy 1000
与原有数进行与运算,即可消除最后的 1 之前的数
xxxx xxxx xxxx 1000
& yyyy yyyy yyyy 1000
= 0000 0000 0000 1000
对于数值 0,如果用原码表示,则有 `+0` 和 `-0` 两种情况
对于减法操作,如 1 - 1,不能转换为加法 1 + (-1)
原码:0 0001 + 1 0001 = 1 0010 = -2
x补 + y补 = (x + y)补,(x - y)补 = x补 + (-y)补
计算 1 - 1,即 1 + (-1)
原码:0 0001 + 1 0001 = 1 0010 = -2
反码:0 0001 + 1 1110 = 1 1111 = [1 0000]原 = -0 // 反码还有 +0
补码:0 0001 + 1 1111 = 0 0000 = [0 0000]原 = 0 // 补码的 0 只有一种表示
-0
,规定在补码中表示为其最小数减一,以 8 位补码为例,规定 1000 0000
表示 -128[0000 0000]补 ~ [0111 1111]补:[0000 0000]原 ~ [0111 1111]原,即 0 ~ 127
[1000 0000]补:原码无法表达(1 1000 0000),规定用来表示 -128
[1000 0001]补 ~ [1111 1111]补:[1111 1111]原 ~ [1000 0001]原,即 -127 ~ -1
127 + 1 => [0111 1111]补 + [0000 0001]补 = [1000 0000]补 = -128
127 + 2 => [0111 1111]补 + [0000 0010]补 = [1000 0001]补 = -127
-128 - 1 => -128 + (-1) => [1000 0000]补 + [1111 1111]补 = [0111 1111]补 = 127
-128 - 2 => -128 + (-2) => [1000 0000]补 + [1111 1110]补 = [0111 1110]补 = 126
~
符号按位取反~4 => 对 4 的补码按位取反
[0 0100]原 => [0 0100]补 => 按位取反 => [1 1011]补
=> 负数补码数值位取反再加 1(或者减 1 得到反码再取反)即为原码,即 [1 0101]原,-5
assert(~4 == -5);
~-4 => 对 -4 的补码按位取反
[1 0100]原 => [1 1100]补 => 按位取反 => [0 0011]补
=> 正数补码等于原码,即 [0 0011]原,3
assert(~-4 == 3);
lowbit
可以直观地使用如下写法int lowbit(int n) { return n & (-n); }
int main() {
assert(lowbit(0b10001101) == 1);
assert(lowbit(0b11011010) == 2);
assert(lowbit(0b10101100) == 4);
assert(lowbit(0b10111000) == 8);
assert(lowbit(0b11010000) == 16);
}
TN 表示以其所在节点为根节点的所有叶子节点的和
__________ T8
/ /|
/ / |
/ / |
/ / |
/ / |
/ / |
_____ T4 / |
/ | / |
/ /| / /|
T2 / | T6 / |
/ | / | / | / |
/ | / | / | / |
T1 | T3 | T5 | T7 |
| | | | | | | |
A1 A2 A3 A4 A5 A6 A7 A8
T1 = A1
T2 = A1 + A2
T3 = A3
T4 = A1 + A2 + A3 + A4
T5 = A5
T6 = A5 + A6
T7 = A7
T8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
可见,TN 表示的是一个连续子序和,序列结束位置为 AN,包含的元素数量为 lowbit(N)
lowbit(1) = 1
lowbit(2) = 2
lowbit(3) = 1
lowbit(4) = 4
lowbit(5) = 1
lowbit(6) = 2
lowbit(7) = 1
lowbit(8) = 8
查找一个位置的所有前缀和,比如对于 7,前缀和为 A1 + A2 + A3 + A4 + A5 + A6 + A7
=> 初始值为 0,开始累加
=> 加上 T7,T7 包含 lowbit(7) = 1 个元素,因此下一步的位置为 7 - 1 = 6
=> 加上 T6,T6 包含 lowbit(6) = 2 个元素,因此下一步的位置为 6 - 2 = 4
=> 加上 T4,T4 包含 lowbit(4) = 4 个元素,因此下一步的位置为 4 - 4 = 0,结束
=> 结果为 T7 + T6 + T4 = A7 + A5 + A6 + A1 + A2 + A3 + A4 = A1 + A2 + A3 + A4 + A5 + A6 + A7
增减一个位置的值时,需要对其所在节点及其所有祖先节点做相同增减,而 N 的父节点即为 N + lowbit(N)
=> A1 += x
=> T1 += x,lowbit(1) = 1,因此 1 的父节点为 1 + 1 = 2
=> T2 += x,lowbit(2) = 2,因此 2 的父节点为 2 + 2 = 4
=> T4 += x,lowbit(4) = 4,因此 4 的父节点为 4 + 4 = 8
=> T8 += x,lowbit(8) = 8,8 + 8 = 16,超出范围,结束
树的深度为 logN / log2 + 1,因此查找和增减操作的复杂度为 O(log n)
class FenwickTree {
public:
void init(int n) {
t.clear();
t.resize(n);
t.shrink_to_fit();
}
void add(int pos, int value) {
while (pos < size(t)) {
t[pos] += value;
pos += lowbit(pos);
}
}
int prefix_sum(int pos) const {
int res = 0;
while (pos) {
res += t[pos];
pos -= lowbit(pos);
}
return res;
}
int range_sum(int from, int to) const {
return prefix_sum(to) - prefix_sum(from - 1);
}
void print_all_prefix_sum() const {
for (int i = 1; i < size(t); ++i) {
cout << prefix_sum(i) << ' ';
}
cout << endl;
}
private:
int lowbit(int x) const { return x & (-x); }
private:
vector<int> t;
};
int main() {
FenwickTree t;
vector<int> v{2, 3, 5, 1, 4, 7, 8, 6};
t.init(size(v) + 1);
for (int i = 0; i < size(v); ++i) {
t.add(i + 1, v[i]);
}
t.print_all_prefix_sum(); // 2 5 10 11 15 22 30 36
cout << t.range_sum(2, 3) << endl; // 8 = 3 + 5
t.add(3, 10);
t.print_all_prefix_sum(); // 2 5 20 21 25 32 40 46
cout << t.range_sum(2, 3) << endl; // 18
}
nums
做离散化处理,映射为一个递增序列,倒序遍历 nums
,查找 nums[i]
对应于序列中的下标 x
,x
即表示有 nums
中有 x
个比 nums[i]
小的数。对树状数组做 add(x + 1, 1)
操作做标记,查找前缀和时,标记过的位置表示在 nums
中遍历过,前缀和即为 nums
中右侧比当前元素小的元素数量class Solution {
public:
vector<int> countSmaller(vector<int>& nums) {
vector<int> helper{nums};
sort(begin(helper), end(helper));
helper.erase(unique(begin(helper), end(helper)), end(helper));
int sz = size(nums);
init(sz + 1);
vector<int> res;
for (int i = sz - 1; i >= 0; --i) {
int x = lower_bound(begin(helper), end(helper), nums[i]) - begin(helper);
res.emplace_back(prefix_sum(x));
add(x + 1, 1);
}
reverse(begin(res), end(res));
return res;
}
void init(int n) {
t.clear();
t.resize(n);
t.shrink_to_fit();
}
void add(int pos, int value) {
while (pos < size(t)) {
t[pos] += value;
pos += lowbit(pos);
}
}
int prefix_sum(int pos) const {
int res = 0;
while (pos) {
res += t[pos];
pos -= lowbit(pos);
}
return res;
}
private:
int lowbit(int x) const { return x & (-x); }
private:
vector<int> t;
};