跳表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
const double PFACTOR = 0.25;
const int MAX_LEVEL = 32;
class Skiplist {
struct Node {
int val;
vector<Node*> forward;
Node(int _val, int level): val(_val), forward(level, nullptr) {
}
};
random_device rd;
mt19937 gen{rd()};
uniform_real_distribution<double> dis{0, 1};
public:
Node* head;
int level;
Skiplist() {
head = new Node(-1, MAX_LEVEL);
level = 0;
}
bool search(int target) {
Node *cur=this->head;
for(int i=level-1; i>=0; i--) {
while(cur->forward[i] && cur->forward[i]->val < target) {
cur = cur->forward[i];
}
}
cur = cur->forward[0];
return cur && cur->val == target;
}
void add(int num) {
int lv=randomLv();
level=max(level, lv);
array<Node*, MAX_LEVEL> shouldupdate;
Node *cur=this->head;
for(int i=level-1; i>=0; i--) { // 找到各层要更新的结点
while(cur->forward[i] && cur->forward[i]->val < num) {
cur = cur->forward[i];
}
shouldupdate[i] = cur; // 虽然全部记录了但是并不是全部都用了
}
Node *newNode=new Node(num, lv);
for(int i=0; i<lv; i++) { // 只有前Lv层(结点所在那些层级)的前序节点才会被更新
newNode->forward[i] = shouldupdate[i]->forward[i];
shouldupdate[i]->forward[i] = newNode;
}
return;
}

bool erase(int num) {
array<Node*, MAX_LEVEL> shouldupdate;
Node *cur=this->head;
for(int i=level-1; i>=0; i--) {
while(cur->forward[i] && cur->forward[i]->val < num) {
cur = cur->forward[i];
}
shouldupdate[i] = cur;
}
cur = cur->forward[0]; // 最底层再推进一次应该就是要删除的元素
if (!(cur && cur->val == num)) return false; // 说明当前skiplist不存在该元素
// 从最底0层开始更新前序节点,直到没有这个元素的层数上
for(int i=0; i<level && shouldupdate[i]->forward[i]==cur; i++) {
shouldupdate[i]->forward[i] = cur->forward[i];
}
delete cur; // 删除节点本身
while(level>1 && !head->forward[level-1]) { // 如果删除节点使得顶上几层变为了空,我们需要删除顶上这些层
level--;
}
return true;
}
private:
int randomLv() {
int lv=1;
while(dis(gen)<PFACTOR && lv<MAX_LEVEL) lv++;
return lv;
}
};

/**
* Your Skiplist object will be instantiated and called as such:
* Skiplist* obj = new Skiplist();
* bool param_1 = obj->search(target);
* obj->add(num);
* bool param_3 = obj->erase(num);
*/

Fisher-Yates 洗牌算法

可以原地实现打乱数组。

1
2
3
4
5
6
7
vector<int> nums; int n=nums.size();
for(int i=0; i<n; i++) {
uniform_int_distribution<> dis(i, n-1); // range is [i, n-1]
int j=dis(gen);
swap(nums[i], nums[j]);
}
return nums;

该程序循环n次,每次从[i, n-1]选取一个数字,然后把它交换到第i位。

概率分析:
原数组中的任意一个数字,在第i次被选出从而放到第i个位置的概率是

1
2
3
4
P(0)=1/n
P(1)=(n-1/n)*(1/n-1)=1/n
...
P(i)= (n-1/n)*(n-2/n-1)*...*(1/n-i)=1/n

所以每个数字出现在每个位置的概率等同。

全排列中获取下一个排列的实现(c++ STL next_permutation)

维基百科中的步骤:

  1. Find the largest index k such that a[k] < a[k + 1]. If no such index exists, the permutation is the last permutation.
  2. Find the largest index l greater than k such that a[k] < a[l].
  3. Swap the value of a[k] with that of a[l].
  4. Reverse the sequence from a[k + 1] up to and including the final element a[n].

至于为什么这么做就可以得到下一个排列,可以看代码及注释。

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution {
public:
void nextPermutation(vector<int>& nums) {
if(nums.size()==1) return;
// find the largest index i which statisfies nums[i]<nums[i+1]
// 从后往前找到交换点i,交换点i以后的数全部都是降序的
// 因为i后面的数[i+1, n)已经无法变得更大,所以我们不得不让i位置变大一点来得到下一个序列
int i=nums.size()-2;
while(i>=0 && nums[i]>=nums[i+1]) {
i--;
}
if(i<0) {
// 如果找不到这么一个位置i,表示整个序列都是降序的,即已经是最后一个排列。
// 重新得到第一个排列的方法是整个数组逆序。
reverse(nums.begin(), nums.end());
return;
}
// 可想而知,为了是变大程度尽量小,i前面的数不应该改动,我们从i后面的数里面挑出一个比i稍大的
// 因为后面是降序,所以可以直接按位置查找
// find the largest index j which statisfies nums[j]>nums[j]
int j=nums.size()-1;
while(nums[j] <= nums[i]) {
j--;
}
// 我们将这个数nums[j]交换到nums[i]的位置
swap(nums[i], nums[j]);
// 因为我们找到的nums[j]是比nums[i]刚刚稍小的数,所以交换完成后,后面的区间[i+1,n]仍然是降序的
// 此时还没有结束。因为后面的区间[i+1,n]是降序的,我们知道降序是最大的排列(最后一个排列)而不是最小的排序
// 我们将这个降序区间调整回为升序区间,此时才是下一个排列
reverse(nums.begin()+i+1, nums.end());
}
};

相关练习题:https://leetcode.cn/problems/next-permutation

参考资料:
https://leetcode.cn/problems/next-permutation/solution/xia-yi-ge-pai-lie-by-leetcode-solution/
https://leetcode.cn/problems/next-permutation/solution/xia-yi-ge-pai-lie-by-powcai/
https://en.wikipedia.org/wiki/Permutation#Generation_in_lexicographic_order

wsl2配置代理

wsl1中主机和wsl共享网络,但在wsl2中是单独的网络。
可以借助如下命令配置代理(可写在~/.profile里)

1
2
3
4
5
6
7
8
9
10
11
host_ip=$(cat /etc/resolv.conf |grep "nameserver" |cut -f 2 -d " ")

# system proxy
export ALL_PROXY="http://$host_ip:10809"

# go proxy
GOPROXY="https://goproxy.cn,direct"

# git proxy
git config --global http.proxy socks5://$host_ip:10808
git config --global https.proxy socks5://$host_ip:10808

记得打开代理软件的接受来自局域网的连接

Ctrl+Z、Ctrl+C、Ctrl+D的区别

  Ctrl+C和Ctrl+Z都是中断命令,但作用不同。

  Ctrl+C是发送SIGINT信号,终止一个进程。

  Ctrl+Z是发送SIGSTOP信号,挂起一个进程,将作业放置到后台(暂停状态)。与此同时,可以通过fg重启前台被中断的任务,也可以通过bg把中断的任务放到后台执行。

  Ctrl+D表示一个特殊的二进制值EOF,代表输入完成或注销。

http://t.zoukankan.com/diantong-p-5566305.html

二分的整理

模板lower_bound & upper_bound

“搜索区间”定义为等于target的区间,对于upper_bound,它的结果是搜索区间外的下一个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
// 搜索区间为 [left, right]
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
// 搜索区间变为 [mid+1, right]
left = mid + 1;
} else if (nums[mid] > target) {
// 搜索区间变为 [left, mid-1]
right = mid - 1;
} else if (nums[mid] == target) {
// 收缩右侧边界
right = mid - 1;
}
}
// 检查出界情况
// 所有可能性:left: [0, n] right: [-1, n-1] mid: [0, n-1]
if (left >= nums.length || nums[left] != target)
return -1;
return left;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 这里改成收缩左侧边界即可
left = mid + 1;
}
}
// 这里改为检查 right 越界的情况,见下图
if (right < 0 || nums[right] != target)
return -1;
return right; // 若要求upper_bound(下一个不等于target的位置)则是return left;
}

1、为什么 while 循环的条件中是 <=,而不是 <?

LeetCode - 954. 二倍数对数组

给定一个长度为偶数的整数数组 arr,只有对 arr 进行重组后可以满足 “对于每个 0 <= i < len(arr) / 2,都有 arr[2 * i + 1] = 2 * arr[2 * i]” 时,返回 true;否则,返回 false。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/array-of-doubled-pairs
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

阅读更多...

分治策略与主定理

== 支配理论 ==
假设有递归关系式 $T(n) = a T\left(\frac{n}{b}\right) + f(n)$,其中$ a \geq 1 \mbox{, } b > 1$

其中,n 为问题规模, a 为递归的子问题数量,$\frac{n}{b}$ 为每个子问题的规模(假设每个子问题的规模基本一样),f(n) 为递归以外进行的计算工作。

=== 情形一 ===
如果存在常数$ \epsilon > 0 $,有

$ f(n) = O\left( n^{\log_b (a) - \epsilon} \right)$(可不严谨地视作多项式地小于)

则: $ T(n) = \Theta\left( n^{\log_b a} \right) $

=== 情形二 ===
如果存在常数$ \epsilon\ge0 $,有: $f(n) = \Theta\left( n^{\log_b a} \log^{\epsilon} n \right)$

则: $T(n) = \Theta\left( n^{\log_b a} \log^{\epsilon+1} n \right) $

=== 情形三 ===
如果存在常数$ \epsilon > 0 $,有: $ f(n) = \Omega\left( n^{\log_b (a) + \epsilon} \right) $(多项式地大于)

同时存在常数$c < 1$以及充分大的$n$,满足:$a f\left( \frac{n}{b} \right) \le c f(n)$

则:$T\left(n \right) = \Theta \left(f \left(n \right) \right)$

== 常用算法中的应用 ==

算法 递归关系式 运算时间 备注
二分搜索 $ T(n) = T\left(\frac{n}{2}\right) + \Theta(1) $ $\Theta(\log n)$ 情形二($\epsilon=0$)
二叉树遍历 $T(n) = 2 T\left(\frac{n}{2}\right) + \Theta(1) $ $\Theta(n)$ 情形一
最佳排序矩阵搜索(已排好序的二维矩阵) $T(n) = 2 T\left(\frac{n}{2}\right) + O(\log n)$
归并排序 $T(n) = 2 T\left(\frac{n}{2}\right) + \Theta(n)$ $\Theta(n\log n)$ 情形二($\epsilon=0$)

最大子数组和

经典题目:给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

注意:题目要求子数组最少包含一个元素,所以返回结果可能为负(若所有元素负,则将返回最大的一个负数)。倘若允许子数组为空(结果为0),则要怎么修改?

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

https://leetcode-cn.com/problems/maximum-subarray/

解1 动态规划:

1
2
3
4
5
6
7
8
9
10
int maxSubArray(vector<int>& nums) {
if(nums.empty()) return 0;
size_t n=nums.size();
vector<int> ans(n); // 代表以nums[i]为结尾的连续最大和
ans[0]=nums[0];
for(int i=1;i<n;i++) {
ans[i]=max(ans[i-1]+nums[i],nums[i]);
}
return *max_element(ans.begin(), ans.end());
}

刻画解的状态结构:f(i)代表以nums[i]为结尾的连续最大和
状态转移方程:f(i)=max{ f(i-1)+nums[i], nums[i] }
解释:选择是将nums[i]以nums[i-1]为结尾的最优解,或是选择以nums[i]单独成为解

注:我在第一次尝试解答时,将状态结构f(i)构造为了 代表以nums[1]…num[i]可以包含的最优解,
然而后续遭遇了困难,因为我无法简单确定nums[1]…num[i-1]的最优解位置,可能不在末尾而在中间,难以继续转移。
以后要注意不同的结构构造方法,并且注意怎样构造才更方便更简洁。

解2 分治法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int maxCrossingSubArray(vector<int>& nums, int left, int mid, int right) {
int leftSum=0;
for(int i=mid-1, sum=0; i>=left; i--) {
sum+=nums[i];
if(sum > leftSum) {
leftSum = sum;
}
}
int rightSum=0;
for(int i=mid+1, sum=0; i<=right; i++) {
sum+=nums[i];
if(sum > rightSum) {
rightSum = sum;
}
}
return leftSum+nums[mid]+rightSum;
}
int maxSubArray(vector<int>& nums, int left, int right) {
if (left==right) {
return nums[left]; // base case
}
int mid = (left+right)/2;
int rLeft = maxSubArray(nums, left, mid);
int rRight = maxSubArray(nums, mid+1, right);
int rMid = maxCrossingSubArray(nums, left, mid, right);
return max(max(rLeft, rRight), rMid);
}
int maxSubArray(vector<int>& nums) {
return maxSubArray(nums, 0, nums.size()-1);
}

注:
maxCrossingSubArray的实现
注意左右的最大和要分开计算。想想若使用同一个sum变量会发生什么?
要注意解中肯定可能包含负数,并非遇到负数就要中断。如3 -1 5肯定比3大。

解3 分治:
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/maximum-subarray/solution/zui-da-zi-xu-he-by-leetcode-solution/
著作权归作者所有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct Status {
int lSum, rSum, mSum, iSum;
};

Status pushUp(Status l, Status r) {
int iSum = l.iSum + r.iSum;
int lSum = max(l.lSum, l.iSum + r.lSum);
int rSum = max(r.rSum, r.iSum + l.rSum);
int mSum = max(max(l.mSum, r.mSum), l.rSum + r.lSum);
return (Status) {lSum, rSum, mSum, iSum};
};

Status get(vector<int> &a, int l, int r) {
if (l == r) {
return (Status) {a[l], a[l], a[l], a[l]};
}
int m = (l + r) >> 1;
Status lSub = get(a, l, m);
Status rSub = get(a, m + 1, r);
return pushUp(lSub, rSub);
}

int maxSubArray(vector<int>& nums) {
return get(nums, 0, nums.size() - 1).mSum;
}

该解采用了不同的分治设计,具体可以参见LeetCode答案。

请我喝杯咖啡吧~

支付宝
微信