404频道

学习笔记

问题描述

一个最多包含n个正整数的文件,每个数小于n,其中n为10000000。要求升序排列整数列表,最多使用1MB的内存,运行时间尽可能短。

代码实现

#include <stdio.h>                                                                                                                                                              
#include <stdlib.h>

#define BITSPERWORD 32
#define SHIFT 5
#define MASK 0x1F
#define N 10000000

int a[1 + N/BITSPERWORD] = {0};

/**
 * i>>SHIFT相当于i/32,用于确定i在第几个int数组中
 * 其中i&MASK含义为i%32,用于确定在int中的第几位
 */
void set(int i)
{
    a[i >> SHIFT] |= (1 << (i & MASK));
}

void clr(int i)
{
    a[i >> SHIFT] &= (0 << (i & MASK));
}

int test(int i)
{
    return a[i >> SHIFT] & (1 << (i & MASK));
}

int main(void)
{
    int i;
    while (scanf("%d", &i) != EOF)
    {
        set(i);
    }
    for (i=0; i<N; i++)
    {
        if (test(i))
        {
            printf("%d\n", i);
        }
    }
    return 0;
}

习题5

通过shell命令echo "scale=2; 10000000 / 1024 / 8 / 1024.0" | bc计算该程序运行时至少需要的存储空间为1.19MB,如果仅提供了1MB的存储空间,则需要更改上述程序的处理方式。

可采用多趟算法,多趟读入输入数据,每次完成一步。针对该题,可采用2步来完成,int数组的大小变更为5000000/8,比之前小了一半。第一步处理0-4999999之间的数据,第二步处理5000000-999999之间的数据。

习题6

如果是每个整数至少出现10次,而不是原先的一次。可以使用4bit来统计出现的次数,申请的数组大小变为了10000000/2。只要是每个整数有出现的最多次数上限该种处理方式就合适,当然整数出现的上限不能太大,否则该算法就没有了任何优势。

习题9

对一个大的数组的初始化操作需要耗费一些时间,为了消除数组的初始化,可以通过两个额外的数组来解决,这是典型的用空间换时间的方法。

      +---+---+---+---+---+---+---+----+
data  |   |   | 3 |   | 2 |   | 8 |    |
      +---+---+---+---+---+---+---+----+
                                        
      +---+---+---+---+---+---+---+----+
from  |   |   | 0 |   | 2 |   | 1 |    |
      +---+---+---+---+---+---+---+----+
                                        
      +---+---+---+---+---+---+---+----+
to    | 1 | 5 | 3 |   |   |   |   |    |
      +---+---+---+---+---+---+---+----+
                                        
                    ^                   
                    +                   
                   top                  

上图中data为要初始化的数组,from和to为辅助数组。如果data[i]已经初始化,则from[i]<top,to[from[i]]=i。from是一个简单的标识,to和top确保了from中不会写入内存中的随机内容。

习题11

该题的答案太他妈逗了,为了能够解决两地之间的数据传输瓶颈,作者给出的答案居然是用信鸽传输图片的底片后再将底片放大的方式来代替原先的用汽车运输的方式,这就是中国古代的飞鸽传书啊。

习题12

该题在《三傻大闹宝莱坞》中见过,这跟编程毛线关系也没有啊。

本文以从服务器下载一个文件为例,讲解HTTP的断点续传功能。

客户端IP地址为:192.168.1.2
服务器IP地址为:192.168.1.3

客户端向服务器发送请求

客户端向服务器发送的请求为:

1
2
3
4
5
6
7
GET /deepc.a HTTP/1.1
Host: 192.168.100.189
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.14 Safari/537.36
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6

从中可以看出请求的文件名为deepc.a文件。

客户端向服务器发送具体请求

1
2
3
4
5
6
7
8
9
GET /deepc.a HTTP/1.1
Host: 192.168.100.189
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.14 Safari/537.36
Accept: */*
Referer: http://192.168.100.189/deepc.a
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Range: bytes=0-32767

Range字段表示请求文件的范围为0-32767。

服务器响应

第一次服务器的HTTP响应报文如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET /deepc.a HTTP/1.1
Host: 192.168.1.3
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.14 Safari/537.36
Accept: */*
Referer: http://192.168.1.3/deepc.a
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Range: bytes=0-32767

HTTP/1.1 206 Partial Content
Date: Sun, 04 May 2014 05:14:54 GMT
Server: Apache/2.4.4 (Win32) PHP/5.4.16
Last-Modified: Sat, 03 May 2014 00:43:22 GMT
ETag: "7efce6-4f8742f6ed9b2"
Accept-Ranges: bytes
Content-Length: 32768
Content-Range: bytes 0-32767/8322278
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive

HTTP的状态为206,表示服务器已经处理了部分HTTP相应。其中Content-Range字段表示服务器已经响应了0-32767个字节的文件内容。8322278表示文件的总长度为8322278字节。

客户端继续向服务器发送请求

客户端根据上次HTTP报文中服务器已经返回给的客户端的数据情况继续向服务器发送请求报文,向服务器发送的请求报文内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GET /deepc.a HTTP/1.1
Host: 192.168.100.189
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.14 Safari/537.36
Accept: */*
Referer: http://192.168.100.189/deepc.a
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Range: bytes=32768-8322277
If-Range: "7efce6-4f8742f6ed9b2"

HTTP/1.1 206 Partial Content
Date: Sun, 04 May 2014 05:14:54 GMT
Server: Apache/2.4.4 (Win32) PHP/5.4.16
Last-Modified: Sat, 03 May 2014 00:43:22 GMT
ETag: "7efce6-4f8742f6ed9b2"
Accept-Ranges: bytes
Content-Length: 8289510
Content-Range: bytes 32768-8322277/8322278
Keep-Alive: timeout=5, max=98
Connection: Keep-Alive

Content-Range的内容表示客户端向服务器请求文件中32768-8322277之间的字节数据。

第二次服务器的HTTP响应报文如下:

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 206 Partial Content
Date: Sun, 04 May 2014 05:14:54 GMT
Server: Apache/2.4.4 (Win32) PHP/5.4.16
Last-Modified: Sat, 03 May 2014 00:43:22 GMT
ETag: "7efce6-4f8742f6ed9b2"
Accept-Ranges: bytes
Content-Length: 8289510
Content-Range: bytes 32768-8322277/8322278
Keep-Alive: timeout=5, max=98
Connection: Keep-Alive

表示服务器已经相应完成了32768-8322277之间的数据。

为了避免linux下的控制台程序A死掉,可以通过一个另外一个程序B来监听A程序,当A程序异常退出时将B程序带起来。当然程序设计的最好方式为程序不崩溃,但是程序中存在bug很难避免,该方法还是有一定的实践意义。对于B程序可以通过shell脚本或者单独一个应用程序来解决。本文将通过shell脚本来解决此问题。

shell脚本的内容

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
#!/bin/bash

check_process()
{
# check parameter
if [ $1 = "" ];
then
return -1
fi

# get the running process
process_names=$(ps -ef | grep $1 | grep -v grep | awk '{print $8}')
for process_name in $process_names
do
if [ $process_name = $1 ] ;
then
return 1
fi
done

# not run and run the process
echo "$(date) : process $1 not run, just run it"
$1
return 0
}

while [ 1 ];do
check_process "/usr/bin/app/process" # programe path
sleep 5
done

将shell脚本在脱离控制台下可以运行

一旦断开了控制台,shell脚本就会由于接收到SIGHUP信号而退出。这里有两种思路来解决该问题,一种是通过系统的crontab来定期调用脚本程序,另外一种是通过神奇的screen程序来解决该问题,我这里通过screen程序来解决该问题,具体screen程序的应用见我的另外一篇文章《》。

应用程序为daemon方式运行

为了能够保证该脚本监控多个应用程序,需要将应用程序设置为daemon方式运行,可以调用函数daemon实现。也可以调用单独实现的daemon函数,具体代码如下:

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
void init_daemon(void) 
{
int pid;
int i;

if(pid=fork())
exit(0);//是父进程,结束父进程
else if(pid< 0)
exit(1);//fork失败,退出
//是第一子进程,后台继续执行

setsid();//第一子进程成为新的会话组长和进程组长
//并与控制终端分离
if(pid=fork())
exit(0);//是第一子进程,结束第一子进程
else if(pid< 0)
exit(1);//fork失败,退出
//是第二子进程,继续
//第二子进程不再是会话组长

for(i=0;i< NOFILE;++i)//关闭打开的文件描述符
close(i);
chdir("/tmp");//改变工作目录到/tmp
umask(0);//重设文件创建掩模
return;
}

在github上可以fork别人的项目成为自己的项目,但是当fork的项目更新后自己fork的项目应该怎么怎么更新呢?我从网上看到了两种方式,一种是采用github的web界面中的操作来实现,具体是通过“Pull Request”功能来实现;另外一种是通过在本地合并代码分支的方式来解决。本文将采用第二种方式,以我最近fork的项目为例来说明。

将fork后自己的项目clone到本地

执行git clone https://github.com/kuring/leetcode.git即可将自己fork的代码更新到本地。

fork完成后的远程分支和所有分支情况如下:

1
2
3
4
5
6
7
kuring@T420:/data/git/leetcode$ git remote -v
origin https://github.com/kuring/leetcode.git (fetch)
origin https://github.com/kuring/leetcode.git (push)
kuring@T420:/data/git/leetcode$ git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/master

将fork之前的项目clone到本地

将fork之前的项目添加到本地的远程分支haoel中,执行git remote add haoel https://github.com/haoel/leetcode

再查看一下远程分支和所有分支情况:

1
2
3
4
5
6
7
8
9
kuring@T420:/data/git/leetcode$ git remote -v
haoel https://github.com/haoel/leetcode (fetch)
haoel https://github.com/haoel/leetcode (push)
origin https://github.com/kuring/leetcode.git (fetch)
origin https://github.com/kuring/leetcode.git (push)
kuring@T420:/data/git/leetcode$ git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/master

将远程代码halel分支fetch到本地

执行git fetch haoel,此时的所有分支情况如下,可以看出多了一个remotes/haoel/master分支。

1
2
3
4
5
kuring@T420:/data/git/leetcode$ git branch -a
* master
remotes/haoel/master
remotes/origin/HEAD -> origin/master
remotes/origin/master

将halel分支merge到本地的分支

执行git merge remotes/haoel/master,此时发现有冲突,提示内容如下:

1
2
3
4
uring@T420:/data/git/leetcode$ git merge haoel/master
自动合并 src/reverseInteger/reverseInteger.cpp
冲突(内容):合并冲突于 src/reverseInteger/reverseInteger.cpp
自动合并失败,修正冲突然后提交修正的结果。

之所以出现上述错误,这是由于我在fork之后在本地修正了源代码中的一处bug,而在fork之后到现在的时间间隔内原作者haoel也正好修正了该bug。打开文件后发现存在如下的内容,其实就是代码风格的问题,我这里将错误进行修正。

1
2
3
4
5
36 <<<<<<< HEAD                                                                                                                                                                
37 while( x != 0 ){
38 =======
39 while( x != 0){
40 >>>>>>> haoel/master

如果没有冲突的情况下通过merge命令即会将haoel/master分支合并master分支并执行commit操作。可以通过git status命令看到当前冲突的文件和已经修改的文件。执行git status命令可以看到如下内容,说明未冲突的文件已经在暂存区,冲突的文件需要修改后执行add操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kuring@T420:/data/git/leetcode$ git status
位于分支 master
您的分支与上游分支 'origin/master' 一致。

您有尚未合并的路径。
(解决冲突并运行 "git commit")

要提交的变更:

修改: src/3Sum/3Sum.cpp
修改: src/4Sum/4Sum.cpp
修改: src/LRUCache/LRUCache.cpp
...... // 此处省略了很多重复的
......

未合并的路径:
(使用 "git add <file>..." 标记解决方案)

双方修改: src/reverseInteger/reverseInteger.cpp

解决完冲突后执行add操作后再通过git status命令查看的内容如下。通过git status命令却看不到已经解决的冲突文件,对于这一点我还是很理解,参考文章中的Git 分支 - 分支的新建与合并是可以看到已经解决的冲突文件的,因为执行git add后将解决完成冲突的文件放到了暂存区中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kuring@T420:/data/git/leetcode$ git status
位于分支 master
您的分支与上游分支 'origin/master' 一致。

所有冲突已解决但您仍处于合并中。
(使用 "git commit" 结束合并)

要提交的变更:

修改: src/3Sum/3Sum.cpp
修改: src/4Sum/4Sum.cpp
修改: src/LRUCache/LRUCache.cpp
...... // 此处省略了很多重复的
......

这里冲突后merge操作并没有执行commit操作,需要解决冲突后再手工执行commit操作,此时整个的同步操作就已经完成了。

结尾

如果隔一段时间后又需要同步项目了仅需要执行git fetch haoel命令以下的操作即可。

参考

由于git命令较多,为了便于查阅增加一处git data transprot commands

Git图解

问题

Given n, generate all structurally unique BST’s (binary search trees) that store values 1…n.

For example,
Given n = 3, your program should return all 5 unique BST’s shown below.

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3

分析

要想能够生成多个树并存储到vector中,最容易想到的就是递归算法。要想能够递归,题目中提供的函数仅有一个参数,结合题目不能够完成递归的条件,考虑到unique binary search trees中的解法,需要递归具有两个参数的函数。

考虑到了递归的问题,还需要利用循环不断将树添加到vector中,这编写起来也是比较有难度,需要掌握循环的次数和什么时候将树添加到vector中。

代码

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
vector<TreeNode *> generateTrees(int n) {
vector<TreeNode *> sub_tree = generateTrees(1, n);
return sub_tree;
}

vector<TreeNode *> generateTrees(int low, int high) {
vector<TreeNode *> result;
if (low > high)
{
result.push_back(NULL);
return result;
}
else if (low == high)
{
TreeNode *node = new TreeNode(low);
result.push_back(node);
return result;
}

for (int i=low; i<=high; i++)
{
vector<TreeNode *> left = generateTrees(low, i - 1);
vector<TreeNode *> right = generateTrees(i + 1, high);
for (int j=0; j<left.size(); j++)
{
for (int k=0; k<right.size(); k++)
{
TreeNode *root = new TreeNode(i);
root->left = left[j];
root->right = right[k];
result.push_back(root);
}
}
}
return result;
}

图的存储

在leetcode中图的存储形式如下,这种形式的图只能适合用来存储是连通图的情况,且根据leetcode提供的_{0,1,2#1,2#2,2}_格式的字符串通过程序来自动构造图比较麻烦,预知字符串的含义请移步到leetcode的解释

1
2
3
4
5
6
7
/**
* Definition for undirected graph.
* struct UndirectedGraphNode {
* int label;
* vector<UndirectedGraphNode *> neighbors;
* UndirectedGraphNode(int x) : label(x) {};
* };

本文为了能够用字符串表示所有图,并且便于程序的构造,使用了邻接表的形式来对图进行存储,即可以用来存储有向图,有可以存储无向图。图一个节点的结构如下:

1
2
3
4
5
6
7
8
9
/**
* 图节点的邻接表表示形式
*/
struct GraphNode {
std::string label;
std::vector<GraphNode *> neighbors;
bool visited; // 深度优先搜索和广度优先搜索的遍历都需要visited数组,为了简化程序,直接在节点的存储结构中设置visited变量
GraphNode(std::string x) : label(x), visited(false) {};
};

图的创建方面为了简化算法实现,对程序的效率没做太多关注,算法复杂度稍高。本算法的难点在于对字符串的拆解,并根据字符串找到对应的节点指针。

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
* 图节点的邻接表表示形式
*/
struct GraphNode {
std::string label;
std::vector<GraphNode *> neighbors;
bool visited;
GraphNode(std::string x) : label(x), visited(false) {};
};

/**
* 通过字符串的值,找到该字符串对应的图节点
*/
GraphNode *get_one_node(const std::vector<GraphNode *> &node_vector, std::string str)
{
for (std::vector<GraphNode *>::const_iterator iter = node_vector.begin(); iter != node_vector.end(); iter++)
{
if ((*iter)->label == str)
{
return *iter;
}
}
return NULL;
}

/**
* 时间复杂度高,对图的构建一般效率要求较低
* 对于查找某个节点的邻接点的指针操作可以使用map来提高查询效率
* 或者可以通过不需要初始化所有节点的方式来构造图,而是采用需要哪个节点构造哪个节点的方式
*/
std::vector<GraphNode *> create_graph(std::string str)
{
std::vector<GraphNode *> node_vector;

// init all nodes
for (size_t pos = 0; pos < str.length() - 1;)
{
size_t end = str.find(',', pos);
if (end != std::string::npos)
{
GraphNode *node = new GraphNode(str.substr(pos, end - pos));
node_vector.push_back(node);
}
else
{
break;
}

pos = str.find('#', pos);
if (pos == std::string::npos)
{
break;
}
else
{
pos++;
}
}

// add neighbors in every node
for (size_t pos = 0; pos < str.length() - 1; )
{
GraphNode *current_node = NULL;
size_t current_end = str.find(',', pos);
if (current_end != std::string::npos)
{
current_node = get_one_node(node_vector, str.substr(pos, current_end - pos));
pos = current_end + 1;
}
else
{
break;
}

size_t node_end = str.find('#', pos); // 当前节点的字符串的结束位置
if (node_end == std::string::npos)
{
node_end = str.length();
}
else
{
node_end--;
}

for ( ; ; )
{
current_end = str.find(',', pos);
if (current_end > node_end || current_end == std::string::npos)
{
// 一个节点的最后一个邻接点
current_end = node_end;
}
else
{
current_end--;
}

GraphNode *node = get_one_node(node_vector, str.substr(pos, current_end - pos + 1));
if (node != NULL)
{
current_node->neighbors.push_back(node);
std::cout << current_node->label << " add " << node->label << std::endl;
}
if (current_end == node_end)
{
// 一个节点的最后一个邻接点
break;
}
else
{
pos = current_end + 2; // 该节点之后还有其他邻接点
}
}
pos = node_end + 2;
}
return node_vector;
}

深度优先搜索

深度优先搜索遵循贪心算法的原理,如果孩子节点不为空,则一直遍历下去。

递归算法

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
void DFS_traverse_recursion(GraphNode *node)
{
if (!node->visited)
{
std::cout << node->label << '\t';
node->visited = true;
}
for (std::vector<GraphNode *>::iterator iter = node->neighbors.begin(); iter != node->neighbors.end(); iter++)
{
if (!(*iter)->visited)
{
DFS_traverse_recursion(*iter);
}
}
}

/**
* 图的深度优先搜索的递归形式
*/
void DFS_traverse_recursion(std::vector<GraphNode *> &graph)
{
for (std::vector<GraphNode *>::iterator iter = graph.begin(); iter != graph.end(); iter++)
{
(*iter)->visited = false;
}
for (std::vector<GraphNode *>::iterator iter = graph.begin(); iter != graph.end(); iter++)
{
if (!(*iter)->visited)
DFS_traverse_recursion(*iter);
}
}

非递归算法

使用栈来实现非递归。

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
/**
* 图的深度优先搜索的非递归形式
*/
void DFS_traverse_not_recursion(std::vector<GraphNode *> &graph)
{
for (std::vector<GraphNode *>::iterator iter = graph.begin(); iter != graph.end(); iter++)
{
(*iter)->visited = false;
}

for (std::vector<GraphNode *>::iterator iter = graph.begin(); iter != graph.end(); iter++)
{
std::stack<GraphNode *> node_stack;
if ((*iter)->visited)
{
continue;
}
node_stack.push(*iter);
while (!node_stack.empty())
{
GraphNode *node = node_stack.top();
node_stack.pop();
if (node->visited)
continue;
std::cout << node->label << '\t';
node->visited = true;
/* 使用反向迭代器遍历后将节点加入到栈中 */
for (std::vector<GraphNode *>::reverse_iterator iter2 = node->neighbors.rbegin(); iter2 != node->neighbors.rend(); iter2++)
{
if (!(*iter2)->visited)
{
node_stack.push(*iter2);
}
}
}
}
}

广度优先搜索

该算法不存在递归算法,仅有非递归版本。需要利用队列来保存需要遍历的节点,占用的存储空间稍多。

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
/**
* 图的广度优先搜索的非递归形式
*/
void BFS_traverse_not_recursion(std::vector<GraphNode *> &graph)
{
for (std::vector<GraphNode *>::iterator iter = graph.begin(); iter != graph.end(); iter++)
{
(*iter)->visited = false;
}

for (std::vector<GraphNode *>::iterator iter = graph.begin(); iter != graph.end(); iter++)
{
std::queue<GraphNode *> node_queue;
if ((*iter)->visited)
{
continue;
}
node_queue.push(*iter);
while (!node_queue.empty())
{
GraphNode *node = node_queue.front();
node_queue.pop();
if (node->visited)
continue;
std::cout << node->label << '\t';
node->visited = true;
for (std::vector<GraphNode *>::iterator iter2 = node->neighbors.begin(); iter2 != node->neighbors.end(); iter2++)
{
if (!(*iter2)->visited)
{
node_queue.push(*iter2);
}
}
}
}
}

相关下载

本文相关源码

[TOC]

近期准备复习数据结构和算法的知识,参照了网络上各路大神的学习攻略,大部分算法学习的思路为参照一些经典书籍(如算法导论)并结合一些代码的实践来完成,并未找到一条适合我的算法学习之路。经过思考后决定采用代码编写曾经的教科书中代码实例的方式来学习,曾经接触的算法教科书包括《数据结构(C语言版)》和《计算机算法基础》。一来这些算法已经基本熟悉,只是时间久远有些已经忘记;二来,通过思考后编写代码增强自己的记忆。

同时我编写的这些实例可以作为leetcode上的很多题目的基础,为下一个阶段刷leetcode上的题目打好基础。

树的存储形式包括了顺序存储(采用数组形式)和链式存储,其中链式存储更为灵活,可以表示任意形式的树,本文中的代码将采用树的链式存储方式。

树的构建

树的构建有多种方式,本文使用字符串采用了自顶向下、自左到右的顺序构建树,跟leetcode的形式一致。其中’#’表示该节点为空,如果该节点为空节点,其左右子孩子节点也要用’#’表示,而不能不用任何字符表示。

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
/**
* 二叉树的链式存储结构
*/
struct TreeNode
{
char data;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(char data) : data(data), left(NULL), right(NULL) {}
};

/**
* 构造二叉树,要构造的字符串采用了自顶向下、自左到右的顺序,跟leetcode的形式一致
*/
TreeNode *create_binary_tree(const char *str)
{
if (!str || strlen(str) == 0)
{
return NULL;
}

// 对每个节点分配存储空间
int node_size = strlen(str);
TreeNode **tree = new TreeNode*[node_size];
for (int i=0; i<node_size; i++)
{
if (str[i] == '#')
{
tree[i] = NULL;
}
else
{
tree[i] = new TreeNode(str[i]);
}
}

for (int i=0, j=0; i<node_size && j<node_size; i++)
{
if (tree[i] != NULL)
{
if ((j + 1) < node_size)
{
tree[i]->left = tree[++j];
tree[i]->right = tree[++j];
}
}
else
{
j += 2;
}
}
return *tree;
}

二叉树的先序遍历

递归实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 先序遍历二叉树的递归形式
*/
void preorder_traverse_recursion(TreeNode *root)
{
if (!root)
{
return;
}

printf("%c\t", root->data);

preorder_traverse_recursion(root->left);

preorder_traverse_recursion(root->right);
}

非递归实现

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
/**
* 先序遍历的非递归形式
*/
void preorder_traverse_not_recursion(TreeNode *root)
{
if (!root)
{
return ;
}
std::stack<TreeNode *> tree_stack;
tree_stack.push(root);
while (tree_stack.size() > 0)
{
TreeNode *node = tree_stack.top();
tree_stack.pop();
printf("%c\t", node->data);
if (node->right != NULL)
{
tree_stack.push(node->right);
}
if (node->left != NULL)
{
tree_stack.push(node->left);
}
}
}

二叉树的中序遍历

递归实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 中序遍历二叉树的递归形式
*/
void inorder_traverse_recursion(TreeNode *root)
{
if (!root)
{
return;
}

inorder_traverse_recursion(root->left);

printf("%c\t", root->data);

inorder_traverse_recursion(root->right);
}

非递归实现

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
/**
* 中序遍历二叉树的非递归形式
*/
void inorder_traverse_not_recursion(TreeNode *root)
{
if (!root)
{
return ;
}
std::stack<TreeNode *> tree_stack;
while (root != NULL || tree_stack.size() > 0)
{
// 遍历到左子树的叶子节点
while (root)
{
tree_stack.push(root);
root = root->left;
}

// 遍历栈顶节点
root = tree_stack.top();
tree_stack.pop();
printf("%c\t", root->data);

root = root->right;
}
}

二叉树的后序遍历

递归实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 后序遍历二叉树的递归形式
*/
void postorder_traverse_recursion(TreeNode *root)
{
if (!root)
{
return;
}

postorder_traverse_recursion(root->left);

postorder_traverse_recursion(root->right);

printf("%c\t", root->data);
}

非递归实现

仅用一个栈不能够实现后序遍历非递归算法,需要保存一个上次访问过节点的变量。

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
/**
* 后序遍历二叉树的非递归形式
*/
void postorder_traverse_not_recursion(TreeNode *root)
{
if (!root)
{
return ;
}
std::stack<TreeNode *> tree_stack;
TreeNode *visited = NULL;
while (root != NULL || tree_stack.size() > 0)
{
// 遍历到左子树的叶子节点
while (root)
{
tree_stack.push(root);
root = root->left;
}

root = tree_stack.top();

if (root->right == NULL || root->right == visited)
{
// 如果没有右孩子,或者右孩子刚刚访问过,则访问当前节点
printf("%c\t", root->data);
tree_stack.pop();
visited = root;
root = NULL;
}
else
{
root = root->right;
}
}
}

总结

二叉树遍历的递归形式程序结构类似,编写相对简单。但是递归方法在C语言中存在执行效率差(需要维护函数栈),容易出现栈溢出的异常的问题。任何递归问题问题都可以转化为非递归问题,转化的思路包括了直接转化法和间接转化法。直接转化法可以通过循环来解决,间接转化法需要借助栈加循环来解决。

二叉树遍历的非递归形式相对复杂,二叉树的先序遍历的非递归形式容易理解,二叉树的中序遍历稍微困难,后序遍历的非递归形式最复杂。

相关下载

程序源代码

在Windows下可以通过计算器进行进制之间的转换,非常方便。本文总结Linux下可用的进行进制之间的转换方法。

利用shell

shell脚本默认数值是由10进制数处理,除非这个数字某种特殊的标记法或前缀开头,才可以表示其它进制类型数值。如:以0开头就是8进制、以0x开头就是16进制数。使用“BASE#NUMBER”这种形式可以表示其它进制。BASE值的范围为2-64。

其他进制转10进制

1
2
3
4
5
6
7
8
9
10
11
12
kuring@T420:~$ echo $((16#4000000))
67108864
kuring@T420:~$ echo $((2#111))
7
kuring@T420:~$ echo $((0x10))
16
kuring@T420:~$ echo $((010))
8

// 对进制转换为10进制进行运算,稍显啰嗦
kuring@T420:~$ echo $(($((16#4000000))/1014/1024));
64

利用let命令

let用来执行算数运算和数值表达式测试。可以利用该命令完成简单的计算,并将计算结果赋给其他变量。

1
2
3
4
// 对进制转换为10进制后进行运算,比纯shell方式简洁
kuring@T420:~$ let a=$((16#4000000))/1014/1024;
kuring@T420:~$ echo $a
64

利用bc命令

该命令是一个强大的计算器软件,可以利用其中的ibase和obase进行输入进制的转换,ibase表示输入数字的进制,obase表示输出数字的进制。

1
2
3
4
5
6
// 如果没有制定,则默认为10进制
// 对于16进制,要使用F,而不能使用f
kuring@T420:~$ echo "ibase=16;FF" | bc
255
kuring@T420:~$ echo "obase=2;ibase=16;FF" | bc
11111111

参考文章

linux shell 不同进制数据转换(二进制,八进制,十六进制,base64)

Shell进制转换小结

题目

Memory Limit Exceeded

Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.

push(x) – Push element x onto stack.
pop() – Removes the element on top of the stack.
top() – Get the top element.
getMin() – Retrieve the minimum element in the stack.

错误代码

看到此题目,以为是用实现一个简单的栈结构,于是就直接写下了如下代码,采用了双向链表的方式来实现:

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
class MinStack {
public:
struct Node
{
int data;
Node *next;
Node *pre;
Node() : next(NULL),pre(NULL) {};
};

MinStack()
{
header = new Node();
tail = header;
}

void push(int x) {
Node *node = new Node();
node->data = x;
node->pre = tail;
tail->next = node;
tail = node;
}

void pop() {
Node *pre = tail->pre;
delete tail;
tail = pre;
tail->next = NULL;
}

int top() {
if (tail == header)
{
return -1;
}
return tail->data;
}

int getMin() {
Node *begin = header->next;
int min = INT_MIN;
while(begin)
{
if (min > begin->data)
{
min = begin->data;
}
begin = begin->next;
}
return min;
}

private:
Node *header;
Node *tail;
};

提交后提示Memory Limit Exceeded错误,开始考虑是不是双向链表占用的空间过多。

正确代码

仔细看题目后发现retrieving the minimum element in constant time,即时间复杂度为O(1),上述实现代码时间复杂度为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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class MinStack {
struct MinUnit
{
int count;
int value;
MinUnit(int count, int value) : count(count), value(value) {};
};
public:
void push(int x) {
data_stack.push(x);
if (min_stack.empty() || x < min_stack.top()->value)
{
MinUnit *p_unit = new MinUnit(1, x);
min_stack.push(p_unit);
}
else if (x == min_stack.top()->value)
{
min_stack.top()->count++;
}
}

void pop() {
if (data_stack.top() == min_stack.top()->value)
{
if (min_stack.top()->count == 1)
{
MinUnit *punit = min_stack.top();
min_stack.pop();
delete punit;
}
else
{
min_stack.top()->count--;
}
}
data_stack.pop();
}

int top() {
return data_stack.top();
}

int getMin() {
return min_stack.top()->value;
}

private:
std::stack<int> data_stack;
// 最小栈,为了便于修改栈单元中的count值,采用存储指针方式
std::stack<MinUnit*> min_stack;
};

由于I/O模型现在掌握还不够透彻,本文仅列出了一些I/O模型中的一些常用概念,对于select、poll、epoll和信号驱动I/O等模型理解还不够透彻,待理解透彻后补上。

阻塞I/O与非阻塞I/O

阻塞I/O:当进行I/O操作后,程序处于等待状态,直到需要的资源可用为止。

非阻塞I/O:当进行I/O操作后,函数直接返回,不需要等待。

两种文件描述符准备就绪的通知模式

水平触发通知:如果文件描述符上可以非阻塞地执行I/O系统调用,此时认为它已经就绪。允许在任意时刻重复检测I/O状态,没有必要每次当文件描述符就绪后尽可能多的执行I/O。其中select()和poll()属于水平触发通知模式。

边缘触发通知:如果文件描述符自上次状态检查以来有了新的I/O活动,此时需要触发通知。只有当I/O事件发生后才会得到通知,在另外一个I/O事件到来之前不会得到通知。因此,在接收到一个I/O事件后,程序在某个时刻应该在相应的文件描述符上尽可能多地执行I/O。每个被检查的文件描述符被置为非阻塞模式,在得到I/O事件通知后重复执行I/O操作,直到系统调用返回错误为止。其中信号驱动I/O属于此种类型。

将的通俗一点,假设通过异步I/O读取socket数据,当接收到100个字节的数据后,这时候会触发通知给应用程序。如果应用程序仅读取了50字节的数据,重新调用异步I/O函数时,在水平触发时仍然会得到通知,而采用边缘触发时不会得到通知。

epoll即可以采用水平触发通知方式,也可以采用边缘触发通知方式。

这两个概念对于理解Linux的I/O模型非常重要,但是却是不容易理解。可以通过举例说明:一个管道内收到了数据,注册该管道描述符的epoll返回,但是用户只读取了一部分数据,然后再次调用了epoll。这时,如果是水平触发方式,epoll将立刻返回,因为当前有数据可读,满足IO就绪的要求;但是如果是边沿触发方式,epoll不会返回,因为调用之后还没有新的IO事件发生,直到有新的数据到来,epoll才会返回,用户可以一并读到老的数据和新的数据。

同步I/O与异步I/O

同步I/O:发出I/O操作后,后面操作不能进行,要么等待I/O操作完成,要么放弃I/O操作。后面的操作和当前的操作只能有一个进行。

异步I/O:发出I/O操作后,马上返回继续执行后面的操作,而I/O操作同时执行。

参考文章

0%