Neo's Blog

不抽象就无法深入思考
不还原就看不到本来面目!

0%

https://blog.csdn.net/fencecat/article/details/123530441

“好”的代码与“坏”的代码
虽然对于“什么是优秀的代码“难以形成一致意见,但是这么多年的经验,让我对代
码“好”与“坏”积累了一些自己的看法。
比如说,“好”的代码应该:

1.容易理解;
2.没有明显的安全问题:
3.能够满足最关键的需求;
4.有充分的注释:
5.使用规范的命名;
6.经过充分的测试。

“坏”的代码包括:
1.难以阅读的代码:
2.浪费大量计算机资源的代码:
3.代码风格混乱的代码:
4.复杂的、不直观的代码:
5.没有经过适当测试的代码。

image

image

这种阅读起来的确定性至少有三点好处,第一点是可以减少代码错误;第二点是可以节省我思考的时间;第三点是可以节省代码阅读者的时间。

减少错误、节省时间,是我们现在选择编码方式的一个最基本的原则。

两个考察点:分治、双指针实现快速partition

时间复杂度:

平均 lgn;
最坏:n^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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//快速排序算法模板
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;

int i = l - 1, j = r + 1, x = q[l + r >> 1];
while (i < j)
{
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j), quick_sort(q, j + 1, r);
}

下面这道题目是基于快排思想的TopK,他们的partion逻辑不太一样,其中一个很重要的点在于:
quickselect要求把输入分成了三部分,[<=X] X [>X];
而上面的quicksort只是把输入分成了两部分,[<X],[>=X]或者[<=X], [>X]
分割点在某一区间内,但是不一定在区间的两端;

```cpp
class Solution {
public:
vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
qselect(input, 0, input.size() - 1, k);
vector<int> res(k);
for (int i = 0; i < k; ++i) {
res[i] = input[i];
}
return res;
}

void qselect(vector<int>& q, int l, int r, int k) {
if (l >= r) return;

int i = l, j = r, x = q[l];
while (i < j) {
while(i < j && q[j] > x) j--; //优先从j开始
q[i] = q[j];
while (i < j && q[i] < x) i++; //注意<=,而不能是<
q[j] = q[i]
}
a[i] = x;

//qselect(q, l, j, k);
//qselect(q, j + 1, r, k);

if (i - l + 1 == k ) return;
else if (i - l + 1 < k) qselect(q, j + 1, r, k - (i - l + 1));
else qselect(q, l, j - 1, k);
}
};

对于有序数组,上面把start作为Pivot的方法,会让算法时间复杂度退化为O(N^2),导致超时。
对数组进行shuffle,能解决有序数组的问题,但是无法解决所有元素全部相同的问题。当所有元素都一样时,shuffle无用,时间复杂度还是O(N^2)
于是就有了下面的解法。这种方法,返回了中间区域的左右边界,让问题迅速简化!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

void quick_sort_3part(int q[], int l, int r) {
if (l >= r) return;

swap(q[l], q[l + rand() % (r - l + 1)]); //随机化
int lt = l, gt = r, i = l + 1, x = q[l];
// 6,4,1,3,6,6,6,6,4,3,10,45,32
// | | | |
// l lt i gt
// 三路快排,确定等于主元pivot的左右边界
while (i <= gt) {
if (q[i] < x) swap(q[i++], q[++lt]); //gt左边的元素都比x大,但是gt指向的不一定大
else if (q[i] > x) swap(q[i], q[gt--]);//把比x大的元素,换到后面了;但换回来的元素不一定“满足条件”,所以i不能变,需要再比较一次!
else i++;// 等于主元,则继续
}
swap(q[l], q[lt]);

quick_sort_3part(q, l, lt - 1);
quick_sort_3part(q, gt + 1, r);
}

一群孩子做游戏,现在请你根据游戏得分来发糖果,要求如下:

  1. 每个孩子不管得分多少,起码分到一个糖果。
  2. 任意两个相邻的孩子之间,得分较多的孩子必须拿多一些糖果。(若相同则无此限制)

给定一个数组 arrarr 代表得分数组,请返回最少需要多少糖果。

要求: 时间复杂度为 O(n)O(n) 空间复杂度为 O(n)O(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:
/**
* pick candy
* @param arr int整型vector the array
* @return int整型
*/
int candy(vector<int>& arr) {
//正向遍历一次,满足了中间大于左边时的要求;
vector<int> s(arr.size(), 1);
for (int i = 1;i < arr.size(); i++){
if (arr[i] > arr[i - 1]) {
s[i] = s[i - 1] + 1;
}
}

//逆向遍历一遍,继续满足中间大于右边时的要求
for (int i = arr.size() - 2; i >= 0; i--) {
if (arr[i] > arr[i + 1] && s[i] <= s[i + 1]) {
s[i] = s[i + 1] + 1;
}
}

int res = 0;
for (auto x : s) {
res += x;
}

return res;
}
};

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
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/

class Solution {
public:
/**
*
* @param head ListNode类
* @return ListNode类
*/
ListNode* deleteDuplicates(ListNode* head) {
ListNode* pre = new ListNode(-100000);
ListNode* nHead = pre, *tail = pre;
int preCnt = 2;
while (head) {
if (head->val == pre->val) { //如果当前元素 与 前一个元素;
preCnt++;
} else {
if (preCnt == 1) { //如果前面的元素出现一次
tail->next = pre;
tail = pre;
tail->next = NULL;
}
pre = head;
preCnt = 1;
}
head = head->next;
}

if (preCnt == 1) {
tail->next = pre;
pre->next = NULL;
}

return nHead->next;
}
};

状态:已经填好的行数,每一行填在了哪些位置

选择: 最多N种选择

路径:放置了Queue的位置的列表;

结果集:列表的列表

结束条件: 已经把所有的行数填写完;

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
class Solution {
public:
int res;
int Nqueen(int n) {
res = 0;
dfs(0, n, 0, 0, 0);
return res;
}

void dfs(int u, int n, int col, int pie, int na) {
if (u == n) {
res++;
return;
}

for (int j = 0; j < n; ++j) {
//用位运算来记录状态
//注意对角线的技巧
//左上 y = x + b; b = (y - x + N) 来表现一条线!!
//右下 y = -x + b; b = (y + x) 来表现一条线!!
if ((col & 1 << j) || (pie & (1 << (n + u - j))) || (na & (1 << (u + j))))
continue;
//col,pie,na都是局部变量,不用保存现场
dfs(u + 1, n, col | (1 << j), pie | (1 << (n + u - j)), na | (1 << (u + j)));
}
}
};

总

outplace的都是稳定的;
outplace的需要一定的memory, inplace中的qsort由于是递归的,所有需要lgn的mem,其他的都不要额外的内存

inplace大多数都不稳定,除了冒泡排序、插入排序

算法 思想 衍生问题
quick sort 分治思想,借助双指针在$O(n)$时间内完成partition 线性第k大,线性求中位数
merge sort 分治思想,双指针(稳定的) 逆序数
heap sort 借助容器来实现排序,还有类似的BST中序遍历 topK问题
counting sort 空间换时间,非比较;通过扫描min/max来支持负数,通过累加和+倒序遍历来保持稳定
bucket sort 抽屉原理,稳定排序,适合元素值集合较小的情况,特别适合外部排序 排序后最大间隔问题、大整数文件取中位数问题
radix sort 稳定子排序算法(计数排序、桶排序来实现)对数字进行k遍排序
shell sort 分块思想,缩小增量排序,一种特殊的插入排序;递减序列的选择很重要 适用于基本有序序列
insertion sort 减治思想,扑克牌 适用于基本有序序列
Bubble sort 稳定

归并排序,是一种特别有用的排序算法;

它背后的算法思想是 分治。

归并排序同时也是一种思想,可以解决归并排序思想解决很多问题。

例如,计算逆序数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//归并排序算法模板
void merge_sort(int q[], int l, int r)
{
if (l >= r) return;

int mid = l + r >>1;
merge_sort(q, l, mid);
merge_sort(q, mid + 1, r);

vector<int> tmp(r -l + 1);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r ) {
if (q[i] < q[j]) tmp[k++] = q[i++];
else tmp[k++] = q[j++];
}

while (i <= mid) tmp[k++] = q[i++];
while (j <= r) tmp[k++] = q[j++];

for (i = l, k = 0; i <= r; i++, k++) q[i] = tmp[k];
}

核心参数

1、corePoolSize:线程池中的常驻核心线程数

2、maximumPoolSize:线程池能容纳同时执行的最大线程数,此值必须>=1

3、keepAliveTime:多余的空闲线程的存活时间

当前线程池数量超过corePoolSize时,当空闲的时间达到keepAliveTime值时,多余的空闲线程会被直接销毁直到只剩下corePoolSize个线程为止

4、timeUtile:keepAliveTime的单位

5、workQueue:任务队列,被提交但未被执行的任务

6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。

7、handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数

工作流程

线程池内部是通过队列+线程实现的,当我们利⽤线程池执⾏任务时:

  1. 如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建
    新的线程来处理被添加的任务。
  2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊
    缓冲队列。
  3. 如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数
    量⼩于maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等
    于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
  5. 当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被
    终⽌。这样,线程池可以动态的调整池中的线程数

合理线程数

CPU密集型:CPU核数+1个线程的线程池

IO密集型:a)CPU核数*2;b)CPU核数/(1-阻塞系数)。(阻塞系数:0.8~0.9)

拒绝策略

第一种拒绝策略是 AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出异常 RejectedExecutionException (属于RuntimeException),让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。

第二种拒绝策略是 DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。

第三种拒绝策略是 DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。

第四种拒绝策略是 CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。

第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。

用户增长的前提是你的产品是满足需求的,且与市场是匹配的(用户来了之后才能够留得住),但到达用户存在阻碍。

所以用户增长的主要工作就是:要减少阻碍,降低交易成本(比如认知成本、信任成本、分享成本等等)。

开始前,先问问自己 7 个与增长相关的基础问题,带着问题思考更高效。

(1)XX公司的商业模式是什么?

(2)XX公司所在的社会环境是什么?

(3)XX公司处在企业生命周期的什么阶段?

(4)XX公司的增长战略、增长策略、增长战术是什么?

(5)XX公司的用户是谁?

(6)XX公司当前是怎么做用户增长?

(7)XX公司的北极星指标是什么?

(因为)不同商业模式、不同阶段,则有不同的增长侧重。所以谈用户增长不能不谈商业模式社会大环境企业生命周期