-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
223 lines (223 loc) · 131 KB
/
search.xml
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[MySQL数据库的简单实现]]></title>
<url>%2F2018%2F09%2F01%2Fdatabase%2F</url>
<content type="text"><![CDATA[写在前面 所有应用软件中,数据库是最复杂的,MySQL的手册有3000多页。 B树 & 数据库索引。 数据库以文本形式存储将所有要保存的数据,写入文本文件。这个文本文件就是数据库。 数据库原理二叉搜索树是一种效率非常高的数据结构,他有三个特点: (1)每个节点最多只有两个子树;(2)左子树节点值小于父节点值,右字树节点值大于父节点值;(3)n个节点中找目标值,需要log(n)次比较。 二叉搜索树不适合数据库,因为它的查找效率与层数相关。越处在下层的数据,就越需要多次比较。极端情况下,n个数据需要n次比较才能找到目标值。对于数据库来说,每进入一层就要从磁盘读取一次数据,这非常致命,因为硬盘的读取时间远远大于数据处理时间,数据库读取磁盘的次数越少越好。 B树是二叉树的改进,它的设计思想是,将数据尽量集中在一起,以便一次读取多个数据,减少磁盘操作次数。 B树的三个特点: (1)一个节点可以有多个值;(2)除非数据被填满,否则不会增加新的层;(3)子节点的值与父节点值有严格的对应关系;(父节点有n个值,就有n+1个子节点。 数据库索引数据库以B树格式存储,只解决了按照“主键”查找数据的问题。如果想查找其他字段,就需要建立索引。 索引就是以某个字段为关键字的B树文件。比如有一张学生表,包含学号、姓名两个字段。可以对姓名建立索引文件,该文件以B树格式对姓名进行存储,每个姓名后面是其在数据库中的存储位置。查找姓名时,先从索引中找到对应第几条记录,然后再从表格中读取。 这种索引查找方法,叫做“索引顺序存取方法”。 【看看大佬】(http://blog.codinglabs.org/articles/theory-of-mysql-index.html) MySQL数据库事务是指批量进行一系列数据库操作,只要有一部不成功,整个操作都不成功。所以需要有一个“操作日志”,以便失败时对操作进行回滚。 事务的特性: 事务有以下四个标准属性的缩写ACID,通常被称为: 原子性: 确保工作单元内的所有操作都成功完成,否则事务将被中止在故障点,和以前的操作将回滚到以前的状态。 一致性: 确保数据库正确地改变状态后,成功提交的事务。 隔离性: 使事务操作彼此独立的和透明的。 持久性: 确保提交的事务的结果或效果的系统出现故障的情况下仍然存在。 在MySQL中,事务开始使用COMMIT或ROLLBACK语句开始工作和结束。开始和结束语句的SQL命令之间形成了大量的事务。 悲观锁、乐观锁悲观锁:悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。 乐观锁:乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。 参考http://blog.codinglabs.org/articles/theory-of-mysql-index.html]]></content>
<categories>
<category>编程之美</category>
</categories>
<tags>
<tag>MySQL原理</tag>
<tag>MySQL事务</tag>
</tags>
</entry>
<entry>
<title><![CDATA[MySQL数据库的连接和使用]]></title>
<url>%2F2018%2F08%2F30%2Fconnect_mysql%2F</url>
<content type="text"><![CDATA[写在前面 Centos 7.4下连接本地数据库方法以及操作步骤 数据库表内容的打印 操作步骤1、首先安装了完整的MySQL数据库,和数据库C语言操作的相应API 2、接下来数据库的查找就是把数据表中的内容都找出来 MySQL API使用的一般思路: 1、连接到数据库; 2、拼装SQL语句; 3、把SQL语句发送到服务器; 4、读取并遍历服务器返回的结果; 5、断开连接。 具体操作12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091#include<stdio.h>#include<mysql/mysql.h>#include"cgi-base.h"int main(){/////////// cgi-base.h ////////////////////////// char buf[1024 * 4] = {0};// if(GetQueryString(buf) < 0)// {// fprintf(stderr, "GetQueryString failes\n");// return 1;// }/////////////////////////////////////////////// ////////////////////////////////////////////// //接下来就要进行数据库的查找 //直接把一个数据库表中的所有数据都查出来 //mysql api 使用的一般思路 //1、连接到数据库 //2、拼装sql语句 //3、把sql语句发送到服务器 //4、读取并遍历服务器返回的结果 //5、断开连接 ///////////////////////////////////////////// //1、连接到数据库 MYSQL* connect_fd = mysql_init(NULL); MYSQL* ret = mysql_real_connect(connect_fd, "127.0.0.1", "root", "", "TestDB", 3306, NULL, 0);// 还回IP,就是机器自己//port 端口; 使用Unix方式连接;最后一个参数常常设置为0 if(ret == NULL) { fprintf(stderr, "mysql connect failed\n"); return 1; } fprintf(stderr, "mysql connect OK\n"); //2、拼装sql语句 const char* sql = "select *from TestTable"; //3、把sql语句发送到服务器 int temp = mysql_query(connect_fd, sql); if(temp < 0) { fprintf(stderr, "mysql query failed\n"); return 1; } //4、读取并遍历服务器返回的结果 MYSQL_RES* result = mysql_store_result(connect_fd); if(result == NULL) { fprintf(stderr, "mysql store failes\n"); return 1; } //读取内容 //获取到表有几行几列,获取到结果集合的表结构,获取到每个元素的具>体值 int rows = mysql_num_rows(result); int fields = mysql_num_fields(result); MYSQL_FIELD* field = mysql_fetch_field(result); //获取表头信息 while(field != NULL) { printf("%s\t", field->name); field = mysql_fetch_field(result); } printf("br"); int i = 0; for(; i<rows; ++i) { MYSQL_ROW row = mysql_fetch_row(result); int j = 0; for(;j<fields;++j) { printf("%s\t", row[j]); } printf("br"); } printf("br"); //5、断开连接 mysql_close(connect_fd); return 0;}]]></content>
<categories>
<category>编程之美</category>
</categories>
<tags>
<tag>MySQL连接</tag>
<tag>MySQL表查询</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hash函数与哈希冲突]]></title>
<url>%2F2018%2F08%2F23%2Fhash%2F</url>
<content type="text"><![CDATA[写在前面 散列的概念属于查找,它不以关键字的比较为基本操作,采用直接寻址技术。在理想情况下,查找的期望时间为O(1)。 hash函数就是把任意长的输入字符串变化成固定长的输出字符串的一种函数。输出字符串的长度称为hash函数的位数。 散列(Hashing)通过散列函数将要检索的项与索引(散列,散列值)关联起来,生成一种便于搜索的数据结构(散列表)。 应用目前应用最为广泛的hash函数是SHA-1和MD5,大多是128位和更长。hash函数在现实生活中应用十分广泛。很多下载网站都提供下载文件的MD5码校验,可以用来判别文件是否完整,在一些BitTorrent下载中,软件将通过计算MD5检验下载到的文件片段的完整性,etc。 性质(1)确定性:哈希的散列值不同,那么哈希的原始输入也就不同。 (2)不确定性:同一个散列值很有可能对应多个不同的原始输入。称为“哈希碰撞”。 哈希冲突哈希冲突是不可避免的,因为键的数目总是比索引的数目多,不管是多么高明的算法都不可能解决这个问题。就算键的数目比索引的数目少,必有一个输出串对应多个输入串,冲突还是会发生。 哈希函数构造准则hash函数的构造准则:简单、均匀。 (1)散列函数的计算简单,快速; (2)散列函数能将关键字集合K均匀地分布在地址集{0,1,…,m-1}上,使冲突最小。 哈希优缺点 优点:(查找速度快) 哈希表是种数据结构,它可以提供快速的插入操作和查找操作。第一次接触哈希表时,它的优点多得让人难以置信。不论哈希表中有多少数据,插入和删除(有时包括侧除)只需要接近常量的时间即0(1)的时间级。实际上,这只需要几条机器指令。 对哈希表的使用者一一人来说,这是一瞬间的事。哈希表运算得非常快,在计算机程序中,如果需要在一秒种内查找上千条记录通常使用哈希表(例如拼写检查器)哈希表的速度明显比树快,树的操作通常需要O(N)的时间级。哈希表不仅速度快,编程实现也相对容易。 缺点:(空间大) 哈希表也有一些缺点它是基于数组的,数组创建后难于扩展某些哈希表被基本填满时,性能下降得非常严重,所以程序虽必须要清楚表中将要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中,这是个费时的过程)。 而且,也没有一种简便的方法可以以任何一种顺序〔例如从小到大〕遍历表中数据项。如果需要这种能力,就只能选择其他数据结构。 然而如果不需要有序遍历数据,井且可以提前预测数据量的大小。那么哈希表在速度和易用性方面是无与伦比的。 哈希函数的构造方法(1)直接定址法:取关键字或关键字的某个线性函数值为哈希地址:H(key) = key 或 H(key) = a·key + b其中a和b为常数,这种哈希函数叫做自身函数。 注意:由于直接定址所得地址集合和关键字集合的大小相同。因此,对于不同的关键字不会发生冲突。但实际中能使用这种哈希函数的情况很少。 ####(2)相乘取整法: 首先用关键字key乘上某个常数A(0 < A < 1),并抽取出key.A的小数部分;然后用m乘以该小数后取整。 注意:该方法最大的优点是m的选取比除余法要求更低。比如,完全可选择它是2的整数次幂。虽然该方法对任何A的值都适用,但对某些值效果会更好。Knuth建议选取 0.61803……。 ####(3)平方取中法: 取关键字平方后的中间几位为哈希地址。 通过平方扩大差别,另外中间几位与乘数的每一位相关,由此产生的散列地址较为均匀。这是一种较常用的构造哈希函数的方法。 将一组关键字(0100,0110,1010,1001,0111)平方后得(0010000,0012100,1020100,1002001,0012321)若取表长为1000,则可取中间的三位数作为散列地址集:(100,121,201,020,123)。 ####(4)除留余数法: 取关键字被数p除后所得余数为哈希地址:H(key) = key MOD p (p ≤ m)。 注意:这是一种最简单,也最常用的构造哈希函数的方法。它不仅可以对关键字直接取模(MOD),也可在折迭、平方取中等运算之后取模。值得注意的是,在使用除留余数法时,对p的选择很重要。一般情况下可以选p为质数或不包含小于20的质因素的合数。 (5)随机数法:选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random (key),其中random为随机函数。通常,当关键字长度不等时采用此法构造哈希函数较恰当。 哈希冲突解决方法 散列表的负载因子 α = 填入表中的元素个数 / 散列表的长度。 对于开放定址法,负载因子应严格控制在 0.7 ~ 0.8 以下。超过0.8,查表时的CPU缓存会按照指数曲线上升。 ####(1)开放定址法: 就是在发生冲突后,通过某种探测技术,去依次探查其他单元,直到探查到不冲突为止,将元素添加进去。 假如是在index的位置发生哈希冲突,那么通常有一下几种探测方式: 线性探测法(线性探测再散列) 向后依次探测index+1,index+2…位置,看是否冲突,直到不冲突为止,将元素添加进去。 线性探测容易出现数据“堆积”,即寻找关键码位置时需要许多次比较,导致搜索时间增加。怎么解决:二次探测、双散列法。 平方探测法(二次探测) 不探测index的后一个位置,而是探测2^i位置,比如探测2^0位置上时发生冲突,接着探测2^1位置,依此类推,直至冲突解决。 研究表明表的长度为质数,且负载因子不超过0.5时,新的表项一定能够插入。而且任何一个位置都不会被探测两次。 注意: (1)用开放定址法建立散列表时,建表前须将表中所有单元(更严格地说,是指单元中存储的关键字)置空。 (2)两种探测方法的优缺点。 线性探测法虽然在哈希表未满的情况下,总能保证找到不冲突的地址,但是容易发生二次哈希冲突的现象。比如在处理若干次次哈希冲突后k,k+1,k+2位置上的都存储了数据,那下一次存储地址在k,k+1,k+2,k+3位置的数据都将存在k+3位置上,这就产生了二次冲突。 这里引入一个新的概念,堆积现象是指用线性探测法处理哈希冲突时,k,k+1,k+2位置已存有数据,下一个数据请求地址如果是k,k+1,k+2,k+3的话,那么这四个数据都会要求填入k+3的位置。 平方探测法可以减少堆积现象的发生,但是前提是哈希表的总容量要是素数4n+3才可以。 (2)链地址法(开散列法)基本思想: 链表法就是在发生冲突的地址处,挂一个单向链表,然后所有在该位置冲突的数据,都插入这个链表中。插入数据的方式有多种,可以从链表的尾部向头部依次插入数据,也可以从头部向尾部依次插入数据,也可以依据某种规则在链表的中间插入数据,总之保证链表中的数据的有序性。Java的HashMap类就是采取链表法的处理方案。 例:已知一组关键字为(19,14,23,01,68,20,84,27,55,11,10,79),则按哈希函数 H(key) = key MOD13 和链地址法处理冲突构造所得的哈希表为: (3)再哈希法:(双散列法) 在发生哈希冲突后,使用另外一个哈希算法产生一个新的地址,直到不发生冲突为止。这个应该很好理解。 再哈希法可以有效的避免堆积现象,但是缺点是不能增加了计算时间和哈希算法的数量,而且不能保证在哈希表未满的情况下,总能找到不冲突的地址。 (4)建立一个公共溢出区: 建立一个基本表,基本表的大小等于哈希表的大小。建立一个溢出表,所有哈希地址的第一个记录都存在基本表中,所有发生冲突的数据,不管哈希算法得到的地址是什么,都放入溢出表中。 但是有一个缺点就是,必须事先知道哈希表的可能大小,而且溢出表里的数据不能太多,否则影响溢出表的查询效率。实际上就是要尽量减少冲突。 MD5加密算法MD5是一个安全的散列算法,输入两个不同的明文不会得到相同的输出值,根据输出值,不能得到原始的明文,即其过程不可逆;所以要解密MD5没有现成的算法,只能用穷举法,把可能出现的明文,用MD5算法散列之后,把得到的散列值和原始的数据形成一个一对一的映射表,通过比在表中比破解密码的MD5算法散列值,通过匹配从映射表中找出破解密码所对应的原始明文。 结束语哈希表一旦发生冲突,其性能就会显著下降。因此建立哈希表时必须规避哈希冲突的产生,大多数哈希表的实现都是:第一步,是通过哈希算法将key值转换一个整数以确定数据的存储位置;第二步,检查是否发生哈希冲突,以及确定发生冲突后的处理方案。 ┆ 凉 ┆ 暖 ┆ 降 ┆ 等 ┆ 幸 ┆ 我 ┆ 我 ┆ 里 ┆ 将 ┆ ┆ 可 ┆ 有 ┆ 谦 ┆ 戮 ┆ 那 ┆ ┆ 大 ┆ ┆ 始 ┆ 然 ┆┆ 薄 ┆ 一 ┆ 临 ┆ 你 ┆ 的 ┆ 还 ┆ 没 ┆ ┆ 来 ┆ ┆ 是 ┆ 来 ┆ 逊 ┆ 没 ┆ 些 ┆ ┆ 雁 ┆ ┆ 终 ┆ 而 ┆┆ ┆ 暖 ┆ ┆ 如 ┆ 地 ┆ 站 ┆ 有 ┆ ┆ 也 ┆ ┆ 我 ┆ ┆ 的 ┆ 有 ┆ 精 ┆ ┆ 也 ┆ ┆ 没 ┆ 你 ┆┆ ┆ 这 ┆ ┆ 试 ┆ 方 ┆ 在 ┆ 逃 ┆ ┆ 会 ┆ ┆ 在 ┆ ┆ 清 ┆ 来 ┆ 准 ┆ ┆ 没 ┆ ┆ 有 ┆ 没 ┆┆ ┆ 生 ┆ ┆ 探 ┆ ┆ 最 ┆ 避 ┆ ┆ 在 ┆ ┆ 这 ┆ ┆ 晨 ┆ ┆ 的 ┆ ┆ 有 ┆ ┆ 来 ┆ 有 ┆┆ ┆ 之 ┆ ┆ 般 ┆ ┆ 不 ┆ ┆ ┆ 这 ┆ ┆ 里 ┆ ┆ 没 ┆ ┆ 杀 ┆ ┆ 来 ┆ ┆ ┆ 来 ┆]]></content>
<categories>
<category>编程之美</category>
</categories>
<tags>
<tag>Hash冲突</tag>
<tag>Hash函数</tag>
</tags>
</entry>
<entry>
<title><![CDATA[揭开智能指针底层面纱]]></title>
<url>%2F2018%2F08%2F05%2F%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88%2F</url>
<content type="text"><![CDATA[写在前面 介绍三个智能指针:unique_ptr、shared_ptr、weak_ptr。auto_ptr已经被C++11丢弃了。 C++的内存管理中,当我们写一个new语句时,一般立即跟一个delete,但是也很难保证没有运行到delete就返回了。申请的资源没有释放,就会造成内存泄露,所以就有了智能指针。 智能指针解决了哪些问题: 1、忘记调用delete释放内存。 2、程序异常的进入catch块忘记释放内存。 3、指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。 为什么不建议使用auto_ptr?123auto_ptr<int> px(new int(8));auto_ptr<int> py;py = px; 如果px和py是普通指针,则两个指针将指向同一个动态分配的int对象。这是不能接受的,因为程序可能将试图删除同一个对象两次——一次是px过期时,另一次是py过期时,我们知道,同一块内存是不能delete两次的。要避免这种问题,主要有以下两种方法: C++11中为什么建议摒弃auto_ptr,看下面的例子: 从程序的运行结果来看,当执行完赋值语句py = px后,再去访问px时程序崩溃了。原因就是因为赋值语句py = px使得对象的所有权从px转让给py了,px已经变为空指针了,再去访问px当然会出错了。 是怎么样交给被赋值的指针的呢? auto_ptr重载了等号操作符,由图可知意思是把赋值智能指针的内存交给被赋值智能指针,如下: 那么如果使用unique_ptr或者shared_ptr又会怎样了?还是测试下吧,先看unique_ptr情况: 如果使用unique_ptr,在这种情况下编译会出错,也就是说尽管与auto_ptr一样,unique_ptr也采用所有权模型,但在使用unique_ptr时,程序不会等到运行阶段崩溃,在编译时就将可能潜在的错误暴露给你。 再看看shared_ptr情况: 使用shared_ptr时运行正常,因为shared_ptr采用引用计数,当执行完赋值语句py = px后,px和py都指向同一块内存,只不过在释放空间时因为事先要判断引用计数值的大小因此不会出现多次删除一个对象的错误。 如何选择哪种智能指针? #### unique_ptr: C++11引入了许多便捷的功能,其中也包括这个,在用之前我们可以先看下底层: 可以清楚的看到,unique_ptr中的拷贝构造和赋值操作符delete了,所以也就意味着,他和auto_ptr有区别,控制权唯一,不能随意转换。用法都差不多: 12unique_ptr<Base1> base1(new Base1);unique_ptr<Base1> base2;//但是不能用拷贝构造和等号赋值把base1赋值给base2了 但是如果想切换控制权的话也不是没有办法,我们可以看到还有个这样的函数: 要理解这两个函数,首先要理解c++11引入的move和forward;而要理解move和forward得先理解左值和右值概念。所以还是讲全一点吧(已经了解的就直接跳过可以): 补充知识点:1、左值与右值: 左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式)。需要注意的是,左值是指表达式结束后依然存在的持久对象,而右值是指表达式结束时就不再存在的临时对象。T& 指向的是 lvalue,而 const T& 指向的,却可能是 lvalue 或 rvalue,左值引用&与右值引用&&(右值引用是c++11加上的)。 2、move和forward: 需要明确的是,move函数可以是用于构造函数,也可以用于赋值函数,但都需要手动显示添加。其实move函数用直白点的话来说就是省去拷贝构造和赋值时中间的临时对象,将资源的内存从一个对象移动到(共享也可以)另一个对象。官话是:c++11 中的 move() 是这样一个函数,它接受一个参数,然后返回一个该参数对应的右值引用。 std::forward(u) 有两个参数:T 与 u。当T为左值引用类型时,u将被转换为T类型的左值,否则u将被转换为T类型右值。如此定义std::forward是为了在使用右值引用参数的函数模板中解决参数的完美转发问题。 其实这里说的不够清晰,下次翻译一篇国外的解释,阅读下来就能很好理解move这个概念了,这里先不深入。 回到这张图,这两个函数体也就很明朗了——重载move版本的拷贝构造函数以及重载move版本的等号赋值函数。 意思就是:把右值的对象(right)移动给左值(_myt&),并且右值清空。 那么用法: 1234unique_ptr<Base1> base1(new Base1);unique_ptr<Base1> base2=move(base1);//base1变成emptyunique_ptr<Base1> base3;base3 = move(base2);//base2变成empty 其它的成员函数就不一一赘述,和auto_ptr大致上是相同的。总结,某种程度来说比auto_ptr更为安全,适用部分特殊情况。 shared_ptr:如果完全理解了上面两个ptr的底层,那么shared_ptr的也就容易理解多了。但是和前两者有很大区别——前两者控制权唯一,切换的时候把前面的清除。而shared_ptr不会,照例看下底层: 很显然,可以直接赋值和调用拷贝构造函数,且不会清空原本的智能指针。用法就很简单了: 1234shared_ptr<Base1> base1(new Base1);shared_ptr<Base1> base2=base1;shared_ptr<Base1> base3;base3 = base2;//三个共享一个 有个地方需要注意,当删除一个智能指针时,并不影响其它两个智能指针的继续使用。因为该片内存添加了一个引用计数,每shared_ptr一次,引用计数+1;每次调用析构函数,引用计数减一。直到最后一个智能指针删除,才会释放内存。 注意:1、在继续查看时,你会发现以下两个函数: 其实就是和unique_ptr一样可以通过move来切换控制权,这个时候是切换,不是共享了。 2、接下来继续翻看,还有两个函数: (其实auto_ptr也有,只是一样,没必要截图了)也就是说,auto_ptr和unique_ptr都可以通过move函数转换成shared_ptr类型,当然,一样是切换控制权的形式,即旧的置空。 用法如下: 12auto_ptr<Base1> base1(new Base1);shared_ptr<Base1> base2=move(base1); 简单总结:shared_ptr多个指针指向相同的对象。shared_ptr使用计数机制来表明资源被几个指针共享,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr p4 = new int(1);的写法是错误的 拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存。 get函数获取原始指针;可以通过成员函数use_count()来查看资源的所有者个数。 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环引用,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用在weak_ptr中介绍。 weak_ptr:weak_ptr更像是shared_ptr的助手: 1、他不像其余三种,可以通过构造函数直接分配对象内存;他必须通过shared_ptr来共享内存。 2、没有重载opreator*和->操作符,也就意味着即使分配到对象,他也没法使用该对象 3、不主动参与引用计数,即,share_ptr释放了,那么weak_ptr所存的对象也释放了。 4、使用成员函数use_count()可以查看当前引用计数,expired()判断引用计数是否为空。 5、lock()函数,返回一个shared_ptr智能指针: 也就是让weak_ptr观测shared_ptr智能指针,并且在需要时候通过lock函数返回一个shared_ptr。 文章大部分参考网上的博客。 总结:如上讲了这么多智能指针,有必要对这些智能指针做个总结: 1、在可以使用 boost 库的场合下,拒绝使用 std::auto_ptr,因为其不仅不符合 C++ 编程思想,而且极容易出错。 2、在确定对象无需共享的情况下,使用 boost::scoped_ptr(当然动态数组使用 boost::scoped_array)。 3、在对象需要共享的情况下,使用 boost::shared_ptr(当然动态数组使用 boost::shared_array)。 4、在需要访问 boost::shared_ptr 对象,而又不想改变其引用计数的情况下,使用 boost::weak_ptr,一般常用于软件框架设计中。 5、最后一点,也是要求最苛刻一点:在你的代码中,不要出现 delete 关键字(或 C 语言的 free 函数),因为可以用智能指针去管理。 感谢各位大佬们的干货:c++11智能指针解析——揭开底层面纱,完整理解智能指针 C++11中的智能指针]]></content>
<categories>
<category>编程之美</category>
</categories>
<tags>
<tag>智能指针</tag>
<tag>底层原理</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【回炉重造】C++虚函数和纯虚函数]]></title>
<url>%2F2018%2F08%2F03%2F%E8%99%9A%E5%87%BD%E6%95%B0%E4%B8%8E%E7%BA%AF%E8%99%9A%E5%87%BD%E6%95%B0%2F</url>
<content type="text"><![CDATA[写在前面 C++中的虚函数的作用主要是实现了多态的机制。 多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用相同的代码来实现可变的算法。 C++虚函数 概念用virtual关键字修饰的函数就叫虚函数。 虚函数实现机制: C++编译阶段,没办法知道一个基类的指针或引用所指对象的类型,所以没办法通过这个指针判断调用的虚函数到底是谁的,所以只能通过查找虚函数表来找到函数的入口地址。 一个类,如果有虚函数,那么编译器在编译这个类的时候就会为它添加一个虚函数表,以及指向这个虚函数表的指针。继承这个基类的之类,也会新建一个虚函数表,如果没有重载,那么这个新的虚函数表中的函数指针就被拷贝为父类该函数的地址,否则为新的函数地址。编译器会将这些函数指针在虚函数表中按照基类中该函数出现的次序排列,子类中的虚函数表也将以这种方式排列。 每个有虚函数的类都有一个虚函数表指针pv,当通过指针或引用调用一个虚函数时,先通过pv找到虚函数表,然后根据这个虚函数在虚函数表中的偏移量来找到正确的函数地址,然后再CALL之。 虚函数举例说明虚函数表是在类之外的,一个类的size不包括虚函数表的大小。而虚函数指针则包含在类中,sizeof一个类则会包含一个虚函数表的指针。 测试虚函数表指针: 12345678910111213141516class Base{public: virtual void f1() { cout << "Base::f1()" << endl; } int _base;};int main(){ Base b; b._base = 1; cout << sizeof(b) << endl; // 输出 8(64位VS2013环境) return 0;} 对象 b 的大小为什么为 8 字节? 另外4个字节存放了一个void**类型的指针_vfptr,该指针就是虚表指针虚表指针指向虚函数表,该表存放的就是类Base中的虚函数的地址。 多继承模式下的对象模型:12345678910111213141516171819202122232425262728293031class Base1{public: virtual void f1() { cout << "Base1::f1()" << endl; } int _base1;};class Base2{public: virtual void f1() { cout << "Base2::f1()" << endl; } int _base2;};class Derived :public Base1,public Base2{public: virtual void f1() { cout << "Derived::f1()" << endl; } virtual void f2() { cout << "Derived::f2()" << endl; } int _derived;}; 纯虚函数 概念 纯虚函数的定义: 12345678class Shape{public: virtual double calcArea()//虚函数 {....} virtual double calcPerimeter()=0;//纯虚函数 ....}; 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”。 为什么要引入虚函数1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。 2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。 3、为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。 4、声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。 纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。 5、定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。纯虚函数的意义:让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。 纯虚函数的实现原理: 在虚函数原理的基础上,虚函数表中,虚函数的地址是一个有意义的值,如果是纯虚函数就实实在在的写一个0。 抽象类 含有纯虚函数的类被称为抽象类 含有纯虚函数的类被称为抽象类,比如上面代码中的类就是一个抽象类,包含一个计算周长的纯虚函数。哪怕只有一个纯虚函数,那么这个类也是一个抽象类,纯虚函数没有函数体,所以抽象类不允许实例化对象,抽象类的子类也可以是一个抽象类。抽象类子类只有把抽象类当中的所有的纯虚函数都做了实现才可以实例化对象。 对于抽象的类来说,我们往往不希望它能实例化,因为实例化之后也没什么用,而对于一些具体的类来说,我们要求必须实现那些要求(纯虚函数),使之成为有具体动作的类。 接口类 仅含有纯虚函数的类称为接口类 如果在抽象类当中仅含有纯虚函数而不含其他任何东西,我们称之为接口类。 1、没有任何数据成员2、仅有成员函数3、成员函数都是纯虚函数 接口类理解: 123456789101112131415161718192021222324class Flyable//会飞{public: virtual void takeoff()=0;//起飞 virtual void land()=0;//降落};class Bird:public Flyable{public: .... virtual void takeoff(){....} virtual void land(){....}private: ....};void flyMatch(Flyable *a,Flyable *b)//飞行比赛//要求传入一个会飞对象的指针,此时鸟类的对象指针可以传入进来{ .... a->takeoff(); b->takeoff(); a->land(); b->land();} 上面的代码,定义一个会飞的接口,凡是实现这个接口的都是会飞的,飞行比赛要求会飞的来参加,鸟实现了会飞的接口,所以鸟可以参加飞行比赛,如果复杂点定义一个能够射击的接口,那么实现射击接口的类就可以参加战争之类需要会射击的对象,有一个战斗机类通过多继承实现会飞的接口和射击的接口还可以参加空中作战。 一些重要的问题 问题一:多态中存在的内存泄露如果在圆形的类中定义一个圆心的坐标,并且坐标是在堆中申请的内存,则在mian函数中通过父类指针操作子类对象的成员函数的时候是没有问题的,可是在销毁对象内存的时候则只是执行了父类的析构函数,子类的析构函数却没有执行,这会导致内存泄漏。部分代码如下(想去借助父类指针去销毁子类对象的时候去不能去销毁子类对象) 如果delete后边跟父类的指针则只会执行父类的析构函数,如果delete后面跟的是子类的指针,那么它即会执行子类的析构函数,也会执行父类的析构函数 12345678910111213141516171819202122232425262728293031class Circle:public Shape{public: Circle(int x,int y,double r); ~Circle(); virtual double calcArea(); ....private: double m_dR; Coordinate *m_pCenter; //坐标类指针 ....};Circle::Circle(int x,int y,double r){ m_pCenter=new Coordinate(x,y); m_dR=r;}Circle::~Circle(){ delete m_pCenter; m_pCenter-NULL;}....int main(){ Shape *shape1=new Circle(3,5,4.0); shape1->calcArea(); delete shape1; shape1=NULL; return 0;} 我们必须要去解决这个问题,不解决这个问题当使用的时候都会造成内存泄漏。面对这种情况则需要引入虚析构函数: 123456789101112131415class Shape{public: .... virtual ~Shape();private: ....};class Circle:public Shape{public: virtual ~Circle();//与虚函数相同,此处virtual可以不写,系统将会自动添加,建议写上 ....};.... 这样父类指针指向的是哪个对象,哪个对象的构造函数就会先执行,然后执行父类的构造函数。销毁的时候子类的析构函数也会执行。 virtual关键字可以修饰普通的成员函数,也可以修饰析构函数,但并不是没有限制。 virtual在函数中的使用限制: 1、普通函数不能是虚函数,也就是说这个函数必须是某一个类的成员函数,不可以是一个全局函数,否则会导致编译错误。 2、静态成员函数不能是虚函数 static成员函数是和类同生共处的,他不属于任何对象,使用virtual也将导致错误。 3、内联函数不能是虚函数 如果修饰内联函数 如果内联函数被virtual修饰,计算机会忽略inline使它变成存粹的虚函数。 4、构造函数不能是虚函数,否则会出现编译错误。 问题二:多态的实现原理 虚函数表指针:类中除了定义的函数成员,还有一个成员是虚函数表指针(占四个基本内存单元),这个指针指向一个虚函数表的起始位置,这个表会与类的定义同时出现,这个表存放着该类的虚函数指针,调用的时候可以找到该类的虚函数表指针,通过虚函数表指针找到虚函数表,通过虚函数表的偏移找到函数的入口地址,从而找到要使用的虚函数。 当实例化一个该类的子类对象的时候,(如果)该类的子类并没有定义虚函数,但是却从父类中继承了虚函数,所以在实例化该类子类对象的时候也会产生一个虚函数表,这个虚函数表是子类的虚函数表,但是记录的子类的虚函数地址却是与父类的是一样的。所以通过子类对象的虚函数表指针找到自己的虚函数表,在自己的虚函数表找到的要执行的函数指针也是父类的相应函数入口的地址。 如果我们在子类中定义了从父类继承来的虚函数,对于父类来说情况是不变的,对于子类来说它的虚函数表与之前的虚函数表是一样的,但是此时子类定义了自己的(从父类那继承来的)相应函数,所以它的虚函数表当中管于这个函数的指针就会覆盖掉原有的指向父类函数的指针的值,换句话说就是指向了自己定义的相应函数,这样如果用父类的指针,指向子类的对象,就会通过子类对象当中的虚函数表指针找到子类的虚函数表,从而通过子类的虚函数表找到子类的相应虚函数地址,而此时的地址已经是该函数自己定义的虚函数入口地址,而不是父类的相应虚函数入口地址,所以执行的将会是子类当中的虚函数。这就是多态的原理。 问题三:函数的覆盖和隐藏 父类和子类出现同名函数称为隐藏。 父类对象.函数名(…); //调用父类的函数子类对象.函数名(…); //调用子类的函数子类对象.父类名::函数名(…);//子类调用从父类继承来的函数。 父类和子类出现同名虚函数称为覆盖 父类指针=new 子类名(…); 父类指针->函数名(…);//调用子类的虚函数。 问题四:虚析构函数的实现原理 虚析构函数的特点: 当我们在父类中通过virtual修饰析构函数之后,通过父类指针指向子类对象,通过delete接父类指针就可以释放掉子类对象。 理论前提: 执行完子类的析构函数就会执行父类的析构函数 原理: 如果父类当中定义了虚析构函数,那么父类的虚函数表当中就会有一个父类的虚析构函数的入口指针,指向的是父类的虚析构函数,子类虚函数表当中也会产生一个子类的虚析构函数的入口指针,指向的是子类的虚析构函数,这个时候使用父类的指针指向子类的对象,delete接父类指针,就会通过指向的子类的对象找到子类的虚函数表指针,从而找到虚函数表,再虚函数表中找到子类的虚析构函数,从而使得子类的析构函数得以执行,子类的析构函数执行之后系统会自动执行父类的虚析构函数。这个是虚析构函数的实现原理。 问题五:设计一个不能被继承的类类的构造函数会自动调用父类的构造函数。同样,子类的析构函数也会自动调用父类的析构函数。要想一个类不能被继承,我们只要把它的构造函数和析构函数都定义为私有函数。那么当一个类试图从它那继承的时候,必然会由于试图调用构造函数、析构函数而导致编译错误。 问题六:C语言模拟实现C++的继承与多态一、面向过程编程与面向对象编程的区别C语言是一种典型的面向过程编程语言,而C++确实在它的基础上改进的一款面向对象编程语言,那么,面向过程与面向对象到底有什么样的区别呢? 【从设计方法角度看】面向过程程序设计方法采用函数(或过程)来描述对数据的操作,但又将函数与其操作的数据分离开来。面向对象程序设计方法是将数据和对象的操作封装在一起,作为一个整体来处理。 【从维护角度看】面向过程程序设计以过程为中心,难于维护。面向对象程序设计以数据为中心,数据相对功能而言,有较强的稳定性,因此更易于维护。 二、继承与多态的概念继承:是面向对象最显著的一个特性。继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力,已有类被称为父类/基类,新增加的类被称作子类/派生类。 多态:按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同现方式即为多态。同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。简单说就是允许基类的指针指向子类的对象。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374#pragma once#include <iostream>using namespace std;//C++中的继承与多态struct A{ virtual void fun() //C++中的多态:通过虚函数实现 { cout<<"A:fun()"<<endl; } int a;};struct B:public A //C++中的继承:B类公有继承A类{ virtual void fun() //C++中的多态:通过虚函数实现(子类的关键字virtual可加可不加) { cout<<"B:fun()"<<endl; } int b;};//C语言模拟C++的继承与多态typedef void (*FUN)(); //定义一个函数指针来实现对成员函数的继承struct _A //父类{ FUN _fun; //由于C语言中结构体不能包含函数,故只能用函数指针在外面实现 int _a;};struct _B //子类{ _A _a_; //在子类中定义一个基类的对象即可实现对父类的继承 int _b;};void _fA() //父类的同名函数{ printf("_A:_fun()\n");}void _fB() //子类的同名函数{ printf("_B:_fun()\n");}void Test(){ //测试C++中的继承与多态 A a; //定义一个父类对象a B b; //定义一个子类对象b A* p1 = &a; //定义一个父类指针指向父类的对象 p1->fun(); //调用父类的同名函数 p1 = &b; //让父类指针指向子类的对象 p1->fun(); //调用子类的同名函数 //C语言模拟继承与多态的测试 _A _a; //定义一个父类对象_a _B _b; //定义一个子类对象_b _a._fun = _fA; //父类的对象调用父类的同名函数 _b._a_._fun = _fB; //子类的对象调用子类的同名函数 _A* p2 = &_a; //定义一个父类指针指向父类的对象 p2->_fun(); //调用父类的同名函数 p2 = (_A*)&_b; //让父类指针指向子类的对象,由于类型不匹配所以要进行强转 p2->_fun(); //调用子类的同名函数}]]></content>
<categories>
<category>编程之美</category>
</categories>
<tags>
<tag>虚函数</tag>
<tag>纯虚函数</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【Linux基础命令】你用过哪些Linux常见命令?]]></title>
<url>%2F2018%2F08%2F02%2FLinux%E5%9F%BA%E7%A1%80%E5%91%BD%E4%BB%A4%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[写在前面: ① 文件目录操作类的命令,比如:cd、ls、cp、rm、find、grep、zip、tar、yum、less。 ② 系统权限操作命令,比如:chmod 、chown 、sudo等。 ③ 操作系统级命令,比如:shutdown、uname 、top、du、df、losf、netstat等。 文件目录操作类的命令:cd(1)cd /home 进入 ‘/ home’ 目录’ (2)cd .. 返回上一级目录 (3)cd ../.. 返回上两级目录 (4)cd ~user1 进入个人的主目录 (5)cd - 返回上次所在的目录 ls(1)ls 查看目录中的文件 (2)ls -F 查看目录中的文件 (3)ls -l 显示文件和目录的详细资料 (4)ls -a 显示隐藏文件 (5)ls [0-9] 显示包含数字的文件名和目录名 cp(1)cp file1 file2 复制一个文件 (2)cp dir/* . 复制一个目录下的所有文件到当前工作目录 rm(1)rm -f file1 删除一个叫做 ‘file1’ 的文件’ (2)rmdir dir1 删除一个叫做 ‘dir1’ 的目录’ (3)rm -rf dir1 删除一个叫做 ‘dir1’ 的目录并同时删除其内容 (4)rm -rf dir1 dir2 同时删除两个目录及它们的内容 find、grep这两个命令我早其他博客总结过:Linux下的查找(grep & find) zip(1)zip file1.zip file1 创建一个zip格式的压缩包 (2)zip -r file1.zip file1 file2 dir1 将几个文件和目录同时压缩成一个zip格式的压缩包 (3)unzip file1.zip 解压一个zip格式压缩包 tar(1)tar -cvf archive.tar file1 创建一个非压缩的 tarball (2)tar -cvf archive.tar file1 file2 dir1 创建一个包含了 ‘file1’, ‘file2’ 以及 ‘dir1’的档案文件 (3)tar -tf archive.tar 显示一个包中的内容 (4)tar -xvf archive.tar -C /tmp 将压缩包释放到 /tmp目录下 (5)tar -cvfz archive.tar.gz dir1 创建一个gzip格式的压缩包 (6)tar -xvfz archive.tar.gz 解压一个gzip格式的压缩包 yum(1)yum install package_name 下载并安装一个rpm包 (2)yum list 列出当前系统中安装的所有包 (3)yum update package_name 更新一个rpm包 (4)yum remove package_name 删除一个rpm包 (5)yum update package_name.rpm 更新当前系统中所有安装的rpm包 less(1)less file1 类似于 ‘more’ 命令,但是它允许在文件中和正向操作一样的反向操作 (2)head -2 file1 查看一个文件的前两行 (3)tail -2 file1 查看一个文件的最后两行 (4)cat file1 从第一个字节开始正向查看文件的内容 (5)tac file1 从最后一行开始反向查看一个文件的内容 (6)more file1 查看一个长文件的内容 统权限操作命令chmod 改变文件权限命令 (1)Chmod u+w filename 表示给文件所有者添加写权限 (2)Chmod u-w filename 表示删除文件所有者的写权限 (3)Chmod u=rwx filename 表示设置文件所有者的权限为可读,可写,可执行 (4)chmod u+x,g+w,o+w test.txt 给所有者添加执行的权限,给所有组和其它人添加写权限 chown 改变文件拥有者:chown命令 (1)chown user1 test.txt 比如要修改文件test.txt的拥有者为user1 (2)chown :user1 test.txt 要修改文件test.txt的拥有组为user1 (3)chown user1:user1 test.txt 要同时修改拥有者和拥有组 sudo Linux sudo命令以系统管理者的身份执行指令,也就是说,经由 sudo 所执行的指令就好像是 root 亲自执行。使用权限:在 /etc/sudoers 中有出现的使用者。 (1)-V 显示版本编号 (2)-h 会显示版本编号及指令的使用方式说明 (3)-l 显示出自己(执行 sudo 的使用者)的权限 操作系统级命令shutdown(1)*shutdown -h now 关闭系统 (2)shutdown -c 取消按预定时间关闭系统 (3)shutdown -r now 重启 (4)reboot 重启 (5)logout 注销 uname(1)uname -m 显示机器的处理器架构 (2)uname -r 显示正在使用的内核版本 top这两个命令我早其他博客总结过:Linux基础命令(三)之top详解 du、 df博客总结过:Linux基础命令(二)之du、df详解 losf(1)losf -i:(端口号) losf -i:8080 → 查看这个端口号有哪些进程在访问 netstat(1)netstat -a :列出所有端口(包括监听和未监听的) (2)netstat -l :列出所有处于监听状态的socket(只显示监听端口) (3)netstat -t:仅显示TCP相关选项 (4)netstat -u:仅显示UDP相关选项 (5)netstat -r:显示路由信息,路由表]]></content>
<categories>
<category>工具操作</category>
</categories>
<tags>
<tag>Linux命令</tag>
<tag>Centos下命令</tag>
</tags>
</entry>
<entry>
<title><![CDATA[成长是从认识自己的无知开始的]]></title>
<url>%2F2018%2F08%2F02%2F%E6%88%90%E9%95%BF%E6%98%AF%E4%BB%8E%E8%AE%A4%E8%AF%86%E8%87%AA%E5%B7%B1%E7%9A%84%E6%97%A0%E7%9F%A5%E5%BC%80%E5%A7%8B%E7%9A%84%2F</url>
<content type="text"><![CDATA[写在前面 毒鸡汤奉上。 操作环境:Vim编辑器。 敬请期待]]></content>
<categories>
<category>程序人生</category>
</categories>
<tags>
<tag>随笔</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【排序算法】快速排序(可视化描述,三种方法实现,四种方法优化)]]></title>
<url>%2F2018%2F07%2F29%2F%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F%2F</url>
<content type="text"><![CDATA[写在前面 这篇文章通过动态图的描述详细介绍了快速排序的原理与过程。 代码实现用三种思想完成(分治思想)。 文章中包含了四种优化方式以及实现原理(包括多线程优化)。 该方法的基本思想是: (1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”; (2)分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大; (3)递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。 总结起来就是:挖坑思想 + 分治思想 对于分治思想,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。 最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列。 方法一:第一种方法就是直接选择这个数组的第一个元素或者最后一个元素作为基准进行排序。这种方法也是基本的快排做法,存在效率问题。 12345int SelectPivot(int arr[],int low,int high)//low第一个元素,high最后一个元素{ return arr[low];//选择选取序列的第一个元素作为基准} 如果这个数组有序,那么每次只能使这个序列 -1,就会沦为冒泡排序,时间复杂度变成O(N^2)。 方法二:随机选择一个基准。 1234567891011/*随机选择枢轴的位置,区间在low和high之间*/int SelectPivotRandom(int arr[],int low,int high){ //产生基准的位置 srand((unsigned)time(NULL)); int pivotPos = rand()%(high - low) + low; //把基准位置的元素和low位置元素互换,此时可以和普通的快排一样调用划分函数 swap(arr[pivotPos],arr[low]); return arr[low];} 由于基准的位置是随机的,那么产生的分割也不会总是会出现劣质的分割。在整个数组数字全相等时,仍然是最坏情况,时间复杂度是O(n^2)。 但是数据相等的概率比较小,所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。“随机化快速排序可以满足一个人一辈子的人品需求”。 方法三:三数取中:效率最高的情况就是这个基准能将数组序列划分成等长度的两部分,但是这个数很难找出来,还容易拖慢快排速度。 这样的中值的估计可以通过随机选取三个元素并用它们的中值作为枢纽元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为基准。 给定待排序序列为:8 1 4 9 6 3 5 2 7 0 左边为:8,右边为0,中间为6。 三个数排序后,中间那个数作为基准,则基准为6。 具体思想:对待排序序列中low、mid、high三个位置上数据进行排序,取他们中间的那个数据作为基准,并用0下标元素存储该基准。 123456789101112131415161718192021222324/*函数作用:取待排序序列中low、mid、high三个位置上数据,选取他们中间的那个数据作为枢轴*/int SelectPivotMedianOfThree(int arr[],int low,int high) //三数取中{ int mid = low + ((high - low) >> 1);//计算数组中间的元素的下标 //使用三数取中法选择枢轴 if (arr[mid] > arr[high]) //目标: arr[mid] <= arr[high] { swap(arr[mid],arr[high]); } if (arr[low] > arr[high]) //目标: arr[low] <= arr[high] { swap(arr[low],arr[high]); } if (arr[mid] > arr[low]) //目标: arr[low] >= arr[mid] { swap(arr[mid],arr[low]); } //此时,arr[mid] <= arr[low] <= arr[high] return arr[low]; //low的位置上保存这三个位置中间的值 //分割时可以直接使用low位置的元素作为基准,而不用改变分割函数了} 性能测试方法: 1、随机生成100万个数据对函数进行性能测试; 2、生成100万个相等数据,测试函数性能; 3、生成100万个有序数据,测试函数性能。 优化一:对于很小或者部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排。 截止范围:待排序序列长度N = 10,(在5 - 20之间可能都会存在这样的效率问题) 12345if (high - low + 1 < 10){ InsertSort(arr,low,high); return;}//else时,正常执行快排 优化二:在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割。 待排序序列 1 4 6 7 6 6 7 6 8 6 三数取中选取基准:基准key = 6 本次划分后,未对与key元素相等处理的结果:1 4 6 6 7 6 7 6 8 6 下次的两个子序列为:1 4 6 和 7 6 7 6 8 6 本次划分后,对与key元素相等处理的结果:1 4 6 6 6 6 6 7 8 7 下次的两个子序列为:1 4 和 7 8 7 在一次划分后,把与key相等的元素聚在一起,能减少迭代次数,效率会提高不少。 具体思想: 第一步,在划分过程中,把与key相等元素放入数组的两端 第二步,划分结束后,把与key相等的元素移到枢轴周围 变态代码:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869void QSort(int arr[],int low,int high){ int first = low; int last = high; int left = low; int right = high; int leftLen = 0; int rightLen = 0; if (high - low + 1 < 10) { InsertSort(arr,low,high); return; } //一次分割 int key = SelectPivotMedianOfThree(arr,low,high);//使用三数取中法选择基准 while(low < high) { while(high > low && arr[high] >= key) { if (arr[high] == key)//处理相等元素 { swap(arr[right],arr[high]); right--; rightLen++; } high--; } arr[low] = arr[high]; while(high > low && arr[low] <= key) { if (arr[low] == key) { swap(arr[left],arr[low]); left++; leftLen++; } low++; } arr[high] = arr[low]; } arr[low] = key; //一次快排结束 //把与枢轴key相同的元素移到枢轴最终位置周围 int i = low - 1; int j = first; while(j < left && arr[i] != key) { swap(arr[i],arr[j]); i--; j++; } i = low + 1; j = last; while(j > right && arr[i] != key) { swap(arr[i],arr[j]); i++; j--; } QSort(arr,first,low - 1 - leftLen); QSort(arr,low + 1 + rightLen,last);} 这是一个性能测试分析图:(大神的) 测试数据分析:三数取中选择枢轴+插排+聚集相等元素的组合,效果竟然好的出奇。其实这里,插排的作用还是不怎么大的。 优化三:快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化。 优点:如果待排序的序列划分极端不平衡,递归的深度将趋近于n,而栈的大小是很有限的,每次递归调用都会耗费一定的栈空间,函数的参数越多,每次递归耗费的空间也越多。优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(logn),将会提高性能。 1234567891011121314151617void QSort(int arr[],int low,int high){ int pivotPos = -1; if (high - low + 1 < 10) { InsertSort(arr,low,high); return; } while(low < high) { pivotPos = Partition(arr,low,high); QSort(arr,low,pivotPos-1); low = pivotPos + 1; }}/////注意:在第一次递归后,low就没用了,此时第二次递归可以使用循环代替///// 测试数据分析: 测试数据分析:其实这种优化编译器会自己优化,相比不使用优化的方法,时间几乎没有减少. 优化四:使用并行或多线程处理子序列。(不知道怎么实现,但可以简单理解下处理过程,以及多线程实现存在的问题。) 复杂度问题:当划分均衡时,平均时间复杂度O(nlogn),空间O(logn);当划分完全不均衡时,最坏时间O(n²),空间O(n)。 快排为什么这么快:推荐阅读:数学之美番外篇:快排为什么那样快 总结一下就是三个原因: 堆排序平均最坏时间复杂度都为O(nlogn),但为什么实际应用中快排效果好于堆排? 虽然都是O(nlogn)级别,但是时间复杂度是近似得到的,快排前面的系数更小,所以性能更好些。 堆排比较交换次数更多。 第三个原因也是最主要的原因,和cpu高速缓冲存储器(cache)有关。由计算机组成原理,我们了解过,cpu有一块高速缓存区(cache)。堆排序要经常处理距离很远的数,不符合局部性原理,会导致cache命中率降低,频繁读写内存。 完整代码:点击即可查看]]></content>
<categories>
<category>编程之美</category>
</categories>
<tags>
<tag>数据结构</tag>
<tag>快速排序</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【排序算法】直接插入排序 & 折半插入排序(二分法插入排序)]]></title>
<url>%2F2018%2F07%2F29%2F%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F%2F</url>
<content type="text"><![CDATA[写在前面 排序:就是将一组杂乱无章的数据按照一定的规律(升序或降序)组织起来。 数据表:待排序数据元素的有限集合。 排序码:通常数据元素有多个属性域,其中有一个属性域可用来区分元素,作为排序依据,该域即为排序码。 排序算法的性能评估:一个算法执行时间是衡量算法好坏的重要参数。排序算法的时间开销可用算法中的数据交换次数,和数据移动次数来衡量。 直接插入排序算法 算法思想:当插入第 i 个元素时,前面的 i-1 个元素已经有序。其实直接插入排序就是拿一个数,放到前面有序的数中就可以了。具体怎么放,不管是循环交换两个数的位置,还是先找到位置,再将该位置后面的数顺移,都没毛病的。 #####算法性质: 直接排序算法是稳定的, 算法的时间复杂度为O(n^2), 最好是用于量小、接近有序的数据。 代码实现:12345678910111213141516template<typename T>void INsert_Sort(T *array, int len){ int key = 0; int end = 0; for (int i = 1; i < len; i++) { key = array[i]; for (end = i - 1; (end >= 0) && array[end] > key; end--) { array[end + 1] = array[end]; } array[end + 1] = key; }} 刚在前面直接插入排序算法性质中说最好用于数据量小且基本有序的的情况。当数据量比较大时,时间会大量的耗费在移动和比较元素上,导致性能降低。因此可以从元素比较和移动上来优化算法。 折半插入排序算法 折半插入排序又称二分法插入排序。和直接插入排序算法不同的是:在插入元素时,利用折半搜索法寻找插入位置。 算法思想:过程同直接插入排序,仅仅是在找插入位置时,不是顺序遍历,而是二分法查找位置。因为:如果要找地 i 个元素的插入位置,那么第 i-1 个元素是已经有序的,可以用二分查找来寻找位置。 算法分析: 时间复杂度:折半插入排序仅仅是减少了比较元素的次数,约为O(nlogn),而且该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数n;而元素的移动次数没有改变,它依赖于待排序表的初始状态。因此,折半插入排序的时间复杂度仍然为O(n²),但它的效果还是比直接插入排序要好。 空间复杂度:排序只需要一个位置来暂存元素,因此空间复杂度为O(1)。 代码实现:12345678910111213141516171819202122232425262728void Bin_Insert_Sort(int* a, int n){ int Low; int High; int Middle; for (int i = 1; i < n; ++i) { Low = 0; High = i - 1; // 求取插入位置 while (Low <= High) { Middle = (Low + High) / 2; if (a[Middle] > a[i]) High = Middle - 1; else Low = Middle + 1; } // 插入 for (int j = i - 1; j > High; --j) { swap(a[j], a[j + 1]); //这里也可以挨个移动元素后插入 } }}]]></content>
<categories>
<category>编程之美</category>
</categories>
<tags>
<tag>插入排序</tag>
<tag>数据结构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[长连接与短连接]]></title>
<url>%2F2018%2F07%2F14%2F%E9%95%BF%E8%BF%9E%E6%8E%A5%E4%B8%8E%E7%9F%AD%E8%BF%9E%E6%8E%A5%2F</url>
<content type="text"><![CDATA[写在前面 HTTP的长连接和短连接实质上是TCP的长连接和短连接。 HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议。 1、HTTP协议与TCP协议的关系?? HTTP的长连接和短连接实质上是TCP的长连接和短连接。HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议。 IP层:解决网络路由和寻址问题TCP协议:解决如何在IP层之上可靠的传输数据报,并在另一端收到发送的包,并且顺序与发出顺序一致。TCP有可靠,面向连接特点。 2、如何理解HTTP协议是无状态的??无状态指的是协议对事务处理没有记忆能力,服务器不知道客户端是什么状态。也就是说,打开一个服务器网页和你之前打开它之间没有任何联系。 HTTP是一个无状态的面向连接的协议,无状态不代表HTTP不能保持TCP连接,更不能代表HTTP使用的是UDP协议(无连接协议)。 3、什么是长连接、短连接??在HTTP/1.0 中,默认使用短连接。就是说:浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。 如果客户端浏览器访问某个HTML或者其他类型的Web页面中包含其他Web资源的这种情况,如:JavaScript文件、图像文件、CSS文件等,浏览器遇到这样的Web资源,就会建立一个会话。 从 HTTP/1.1 起,默认使用长连接,保持连接持续性。一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的的网页,会继续使用这一条已经建立好的连接。 Keep-Alive不会永久保持连接,它有一个保持的时间,可以在不同服务器软件中设定这个时间(Apache)。要实现长连接,客户端、服务器端首先得支持。 HTTP协议长连接、短连接实质上是TCP协议的长连接、短连接。 TCP短连接模拟一下TCP短连接的情况,Client向server发起连接请求,server接到请求,然后双方建立连接,Client向server发送消息,server回应client,然后一次读写就完成了,这是双方任意一放都可以发起close请求。一般都是client发起close请求。 短连接优点: 管理起来简单,存在的连接都是有用的,不需要额外的控制手段。 TCP长连接模拟TCP长连接情况:client向server发送连接请求,server接受client连接,双方建立连接,client与server完成一次读写之后,他们并不会主动关闭,后续的读写操作会继续使用这个连接。 首先说一下TCP/IP上说到的TCP保活功能,保活功能主要为服务器应用提供,服务器应用希望知道客户端主机是否崩溃,从而可以代表客户使用资源。如果客户已经消失,使得服务器上保留一半开放的连接,而服务器又在等待来自客户端的数据,保活功能就是试图在服务器端检测这种半开放的连接。 如果给定的连接在两个小时内没有任何的动作,则服务器就向客户端发送一个探测报文段, 客户端必须处于以下四种转态。 1、客户端主机依然正常运行,并从服务器可达,客户的TCP响应正常,而服务器也知道对方是正常的,服务器在两小时之后将保活计时器复位。 2、客户端已经崩溃,并且关闭或者正在重启。在任何一种情况下,客户端的TCP都没有响应,那么服务器端也就不能收到探测响应。并在75秒后超时,服务器共发送10个这样的探测,每个间隔75秒,如果客户端没有收到一个响应,就认为客户端主机已经关闭并终止连接。 3、客户端主机已经崩溃,但是已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。 4、客户端正常运行,但服务器不可达,类似2.TCP能发现的就是没有收到的响应。 短连接步骤:建立连接–数据传输–关闭连接 —— 长连接步骤:建立连接–数据传输–(保持连接)–数据传输–数据传输–关闭连接 —— 4、长连接和短连接的优缺点??由上可知,长连接省去了较多的TCP建立、关闭操作,减少了浪费,节约时间。对于频繁请求资源的客户来说,适合用长连接。不过保活功能探测周期比较长,而且只能探测TCP连接的存活,遇到恶意连接,保活功能就有点不够用了。 在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可 以避免一些恶意连接导致server端服务受损;如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样可以完全避免某个蛋疼的客户端连累后端服务。 短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽。 长连接和短连接的产生在于client和server采取的关闭策略,具体的应用场景采用具体的策略,没有十全十美的选择,只有合适的选择。 5. 什么时候用长连接,短连接?长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。 例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。 而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源。 如果用长连接而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。]]></content>
<categories>
<category>SO Kernal</category>
</categories>
<tags>
<tag>长连接</tag>
<tag>短连接</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【编程之美】链表面试题汇总版]]></title>
<url>%2F2018%2F06%2F21%2F%E3%80%90%E7%BC%96%E7%A8%8B%E4%B9%8B%E7%BE%8E%E3%80%91%E9%93%BE%E8%A1%A8%E9%9D%A2%E8%AF%95%E9%A2%98%E6%B1%87%E6%80%BB%E7%89%88%2F</url>
<content type="text"><![CDATA[写在前面 对于不带头结点的单链表操作,是在面试中经常出现的,在此对于常见链表操作进行总结. 操作:有序链表合并、链表反转、链表排序、找倒数第K个节点、非头节点前插入data、删除非尾节点、约瑟夫环、带环链表环的大小等等. function.h1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768#ifndef __FUNCTION_H__#define __FUNCTION_H__#include<stdio.h>#include<stdlib.h>#include<assert.h>#include<malloc.h>#define DataType intstruct Node{ DataType data; struct Node* _pNext;};typedef struct Node Node;typedef Node *pNode;pNode InitNodeList(pNode *pHead);//初始化pNode BuyNewNode(DataType data);//新建节点void InsertNodeByFrontToTail(pNode *pHead, DataType data);//头插法void PrintNodeProntToTail(pNode pHead);//正序打印void InserttNodeByTailToFront(pNode *pHead, DataType data);//尾插法pNode ReceiveNodeList(pNode pHead);//反转链表—一般方法pNode ReceiveNodeList_DG(pNode pHead);//反转链表——递归实现pNode MeryTwoSortNodeChangeOneSortNode(pNode pHead1, pNode pHead2);//合并两个有序单链表——一般方法pNode MeryTwoSortNodeChangeOneSortNode_DG(pNode pHead1, pNode pHead2);//合并两个有序单链表——递归实现void BubbleSortNodeList(pNode pHead);//链表排序——冒泡法实现pNode SearchMIdNode(pNode pHead);//查找中间节点——快慢指针法pNode FindLastKNode(pNode pHead, int key);//找到倒数第K个节点void InsertNotIntoKHead(pNode pos, DataType data);// 非头结点前插入datapNode FindDataInNodeList(pNode pHead, DataType k);//寻找链表中data为K的节点void DeleteLastKNode(pNode pHead, int key);//删除倒数第K个节点void DeleteNotTailNode(pNode pos);//删除单链表的非尾结点void GetCircleForJoseph(pNode pHead);//构造约瑟夫环void GetJosephCircle(pNode pHead, size_t K);//单链表实现约瑟夫环void GetCircleForList(pNode pHead);//构造带环链表pNode isHaveCircle(pNode pHead);//判断链表是否带环int GetCircleLength(pNode pHead);//求环的长度pNode GetCircleIntoNode(pNode pHead);//求环的入口点pNode pFrontNode(pNode pHead);//返回链表第一个节点pNode pTailNode(pNode pHead); //返回链表最后一个节点void InsertNotHead(pNode pos, DataType data);// 非头结点前插入data#endif function.c123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451#include"function.h"pNode BuyNewNode(DataType data){ pNode pTempNode = NULL; pTempNode = (pNode)malloc(sizeof(pNode)); if (pTempNode) { pTempNode->data = data; pTempNode->_pNext = NULL; } return pTempNode;}void InsertNodeByFrontToTail(pNode *pHead, DataType data){ pNode pTemp = NULL; if (*pHead == NULL) *pHead = BuyNewNode(data); else { pTemp = BuyNewNode(data); pTemp->_pNext = *pHead; *pHead = pTemp; }}void InserttNodeByTailToFront(pNode *pHead, DataType data){ pNode pTest = NULL; pNode pCur = *pHead; pTest = BuyNewNode(data); if (*pHead == NULL) (*pHead) = pTest; else { while (NULL != pCur->_pNext) { pCur = pCur->_pNext; } pCur ->_pNext = pTest; }}void PrintNodeProntToTail(pNode pHead){ //pNode pTemp = pHead; if (NULL == pHead) return; while (pHead != NULL) { printf("%d -> ", pHead->data); pHead = pHead->_pNext; } printf("NULL"); printf("\n");}pNode ReceiveNodeList(pNode pHead){ pNode pTest = pHead; //pNode pNextNode = NULL; pNode pPre = NULL; //pNode pReverse = NULL; if (pHead == NULL || pHead->_pNext == NULL) return pHead; while (pTest != NULL) { pNode pNextNode = pTest->_pNext; pTest->_pNext = pPre; pPre = pTest; pTest = pNextNode; } return pPre;}pNode ReceiveNodeList_DG(pNode pHead){ pNode pNewNode = NULL; if (pHead == NULL || pHead->_pNext == NULL) return pHead; pNewNode = ReceiveNodeList_DG(pHead->_pNext); pHead->_pNext->_pNext = pHead; //翻转链表的指向 pHead->_pNext = NULL; //记得赋值NULL,防止链表错乱 return pNewNode; //新链表头永远指向的是原链表的链尾}pNode MeryTwoSortNodeChangeOneSortNode(pNode pHead1, pNode pHead2){ pNode pNewNode = NULL; pNode pTemp = NULL; pNode pNextNode = NULL; unsigned int Node1Len = 0; unsigned int Node2Len = 0; while (pHead1->_pNext != NULL) { ++Node1Len; } while (pHead1->_pNext != NULL) { ++Node2Len; } if (Node1Len < Node2Len) //长的链表为pHead1 { pTemp = pHead1; pHead1 = pHead2; pHead2 = pTemp; } while (pHead1 != NULL) { if (pHead1->data < pHead2->data) { pNextNode = pHead1; pHead1 = pHead1->_pNext; pNextNode = pNextNode->_pNext; } pNextNode = pHead2; pHead2 = pHead2->_pNext; pNextNode = pNextNode->_pNext; } if (pHead1 == NULL) return pNewNode; else { pNextNode = pHead1; } return pNewNode;}pNode MeryTwoSortNodeChangeOneSortNode_DG(pNode pHead1, pNode pHead2){ pNode pNewHead = NULL; if (pHead1 == NULL) return pHead2; else if (pHead2 == NULL) return pHead1; else { if (pHead1->data > pHead2->data) { pNewHead = pHead2; pNewHead->_pNext = MeryTwoSortNodeChangeOneSortNode_DG( pHead1, pHead2->_pNext); } else { pNewHead = pHead1; pNewHead->_pNext = MeryTwoSortNodeChangeOneSortNode_DG(pHead1->_pNext, pHead2); } } return pNewHead;}void BubbleSortNodeList(pNode pHead){ int pTempNode = 0; pNode pTail = NULL; pNode pPreCur = NULL; pNode pCur = NULL; int isFlag = 1; if (pHead == NULL || pHead->_pNext == NULL) { return; } while (pHead != pTail) { isFlag = 0; pPreCur = pHead; pCur = pHead->_pNext; while (pCur != pTail) { if (pCur->data < pPreCur->data) { pTempNode = pCur->data; pCur->data = pPreCur->data; pPreCur->data = pTempNode; isFlag = 1; } pPreCur = pCur; pCur = pCur->_pNext; } if (!isFlag) return; pTail = pPreCur; }}pNode SearchMIdNode(pNode pHead){ pNode pFast = pHead; pNode pSlow = pHead; if (pHead == NULL || pHead->_pNext == NULL) return NULL; while (NULL != pFast && NULL != pFast->_pNext) { pSlow = pSlow->_pNext; pFast = pFast->_pNext->_pNext; } return pSlow;}pNode FindLastKNode(pNode pHead, int key){ pNode pCur = pHead; pNode pSlow = pHead; pNode pFast = pHead; if (pHead == NULL ) { return NULL; } while (key--) { if (pFast == NULL) return NULL; pFast = pFast->_pNext; } while (pFast != NULL) { pFast = pFast->_pNext; pSlow = pSlow->_pNext; } return pSlow;}void DeleteLastKNode(pNode pHead, int key){ pNode pPosDelNode = FindLastKNode(pHead, key+1); if (pPosDelNode == pHead) { free(pPosDelNode); return; } pPosDelNode->_pNext = pPosDelNode->_pNext->_pNext;}pNode FindDataInNodeList(pNode pHead, DataType k){ pNode pCur = pHead; while (pCur != NULL) { if (pCur->data == k) { return pCur; } pCur = pCur->_pNext; } return NULL;}pNode pFrontNode(pNode pHead) //返回链表第一个节点{ pNode pPcur = pHead; if (pHead == NULL) return NULL; return pPcur;}pNode pTailNode(pNode pHead) //返回链表最后一个节点{ pNode pPcur = pHead; if (pHead == NULL) return NULL; while (pPcur != NULL && pPcur->_pNext != NULL) { pPcur = pPcur->_pNext; } return pPcur;}void GetCircleForList(pNode pHead){ pNode pTemp = NULL; pTemp = FindDataInNodeList(pHead, 9); pTemp->_pNext = FindDataInNodeList(pHead, 7);}pNode isHaveCircle(pNode pHead){ pNode pSlowNode = pHead; pNode pFastNode = pHead; if (pHead == NULL) return NULL; while (pFastNode != pSlowNode && pFastNode->_pNext != NULL) { pFastNode = pFastNode->_pNext->_pNext; pSlowNode = pSlowNode->_pNext; } return pSlowNode;}int GetCircleLength(pNode pHead){ pNode MeetNode = NULL; pNode pCur = NULL; size_t count = 0; if (isHaveCircle(pHead) == NULL) return 0; MeetNode = isHaveCircle(pHead); pCur = MeetNode->_pNext; while (pCur != MeetNode) { count++; pCur = pCur->_pNext; } return count;}pNode GetCircleIntoNode(pNode pHead){ pNode pCur = pHead; //在判断带环问题时,返回了环中快慢指针的相遇点。 pNode pMeetNodeInCircle = NULL; pMeetNodeInCircle = isHaveCircle(pHead); while (pCur != pMeetNodeInCircle) { pCur = pCur->_pNext; pMeetNodeInCircle = pMeetNodeInCircle->_pNext; } return pMeetNodeInCircle;}void GetCircleForJoseph(pNode pHead){ pNode front = NULL; pNode tail = NULL; front = pFrontNode(pHead); tail = pTailNode(pHead); tail->_pNext = front;}void GetJosephCircle(pNode pHead, size_t K){ int count = K; pNode pPreNode = pHead; pNode pCurNode = NULL; if (pHead == NULL) return; while (pPreNode->_pNext != pPreNode) { count = K; while (--count) { pCurNode = pPreNode; pPreNode = pPreNode->_pNext; } pCurNode->_pNext = pPreNode->_pNext; printf("%d 号出去-> ", pPreNode->data); //free(pPreNode); pPreNode = pPreNode->_pNext; } //return pPreNode; printf("\n最后留下的人是: %d \n\n", pPreNode->data);}int GetNodeListLength(pNode pHead){ int count = 0; pNode pCur = pHead; if (pHead == NULL) return 0; while (pCur) { count++; } return count;}void DeleteNotTailNode(pNode pos){ assert(pos); pNode pTempNode = NULL; DataType temp = 0; if (pos == NULL) return; else { //交换节点值 temp = pos->data; pos->data = pos->_pNext->data; pos->_pNext->data = temp; pTempNode = pos->_pNext; pos->_pNext = pTempNode->_pNext; //free(pTempNode); pTempNode = NULL; }}void InsertNotHead(pNode pos, DataType data){ pNode pNewNode = NULL; pNode pCur = NULL; DataType pTemp = 0; if (pos == NULL) return; pNewNode = BuyNewNode(data); if (pNewNode == NULL) return; pCur = pos; pNewNode->_pNext = pCur->_pNext; pCur->_pNext = pNewNode; pTemp = pCur->data; pCur->data = pNewNode->data; pNewNode->data = pTemp;} test.c123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180#include"function.h"void test(){ pNode pHead1 = NULL; pNode pHead2 = NULL; pNode pNewHead = NULL; pNode pHead3 = NULL; pNode pSortNode = NULL; pNode LastKNum = 0; pNode pReverseNode = NULL; pNode pR = NULL; pNode pHead4 = NULL; pNode pCirclieNode = NULL; size_t CircleLength = 0; pNode pIntoCircleNode = NULL; pNode pHead5 = NULL; pNode JueNode = NULL; pNode pHead6 = NULL; pNode pDelNode = NULL; pNode pInsertNode = NULL; pNode pH = NULL; InsertNodeByFrontToTail(&pHead1, 9); InsertNodeByFrontToTail(&pHead1, 8); InsertNodeByFrontToTail(&pHead1, 6); InsertNodeByFrontToTail(&pHead1, 5); InsertNodeByFrontToTail(&pHead1, 2); InsertNodeByFrontToTail(&pHead1, 1); PrintNodeProntToTail(pHead1); printf("\n链表的反转、还原:\n"); pReverseNode = ReceiveNodeList(pHead1); //反转 PrintNodeProntToTail(pReverseNode); pR = ReceiveNodeList_DG(pReverseNode); //反转 PrintNodeProntToTail(pR); pHead1 = pR; printf("\npHead1: "); PrintNodeProntToTail(pHead1); //还原pHead1 printf("\n"); InserttNodeByTailToFront(&pHead2, 2); InserttNodeByTailToFront(&pHead2, 3); InserttNodeByTailToFront(&pHead2, 4); InserttNodeByTailToFront(&pHead2, 5); InserttNodeByTailToFront(&pHead2, 6); InserttNodeByTailToFront(&pHead2, 7); printf("pHead2: "); PrintNodeProntToTail(pHead2); printf("\n两个有序链表的合并:\n"); pNewHead = MeryTwoSortNodeChangeOneSortNode_DG(pHead1, pHead2); PrintNodeProntToTail(pNewHead); //新建无序链表3 InsertNodeByFrontToTail(&pHead3, 9); InsertNodeByFrontToTail(&pHead3, 4); InsertNodeByFrontToTail(&pHead3, 5); InsertNodeByFrontToTail(&pHead3, 7); InsertNodeByFrontToTail(&pHead3, 4); InsertNodeByFrontToTail(&pHead3, 0); InsertNodeByFrontToTail(&pHead3, 3); printf("\npHead3: "); PrintNodeProntToTail(pHead3); printf("\n排序好的链表3:"); BubbleSortNodeList(pHead3); PrintNodeProntToTail(pHead3); printf("\npHead1中间节点:%d\n", (SearchMIdNode(pHead1)->data)); printf("\npNewHead中间节点:%d\n", (SearchMIdNode(pNewHead)->data)); printf("\n查找倒数第K个节点: "); LastKNum = FindLastKNode(pNewHead, 2); if (LastKNum != NULL) printf("%d\n", LastKNum->data); else printf("没有找到\n"); printf("\n删除倒数第K个节点\n"); DeleteLastKNode(pNewHead, 3); //构造一个带环链表4 InsertNodeByFrontToTail(&pHead4, 9); InsertNodeByFrontToTail(&pHead4, 8); InsertNodeByFrontToTail(&pHead4, 6); InsertNodeByFrontToTail(&pHead4, 5); InsertNodeByFrontToTail(&pHead4, 2); InsertNodeByFrontToTail(&pHead4, 1); InsertNodeByFrontToTail(&pHead4, 3); InsertNodeByFrontToTail(&pHead4, 0); InsertNodeByFrontToTail(&pHead4, 7); printf("\n打印链表4:\n"); PrintNodeProntToTail(pHead4); printf("\n构造带环链表\n"); GetCircleForList(pHead4); printf("构造OK\n"); pCirclieNode = isHaveCircle(pHead4); if (pCirclieNode == NULL) printf("不带环\n"); else printf("带环\n"); printf("\n求环的长度:"); //PrintNodeProntToTail(pHead4); CircleLength = GetCircleLength(pHead4); printf("%d", CircleLength); printf("\n求环的入口:"); pIntoCircleNode = GetCircleIntoNode(pHead4); printf("%d\n", pIntoCircleNode->data); //构造一个链表,pHead5: 头插法构造单链表 InsertNodeByFrontToTail(&pHead5, 9); InsertNodeByFrontToTail(&pHead5, 8); InsertNodeByFrontToTail(&pHead5, 7); InsertNodeByFrontToTail(&pHead5, 6); InsertNodeByFrontToTail(&pHead5, 5); InsertNodeByFrontToTail(&pHead5, 4); InsertNodeByFrontToTail(&pHead5, 3); InsertNodeByFrontToTail(&pHead5, 2); InsertNodeByFrontToTail(&pHead5, 1); printf("\n约瑟夫环问题:\n"); //构造环 GetCircleForJoseph(pHead5); printf("构造OK\n"); //PrintNodeProntToTail(pHead5); //约瑟夫环 GetJosephCircle(pHead5, 4); printf("构造链表6\n"); InsertNodeByFrontToTail(&pHead6, 6); InsertNodeByFrontToTail(&pHead6, 5); InsertNodeByFrontToTail(&pHead6, 4); InsertNodeByFrontToTail(&pHead6, 3); InsertNodeByFrontToTail(&pHead6, 2); PrintNodeProntToTail(pHead6); printf("\n删除倒数第 3 个节点后,链表为:\n"); DeleteLastKNode(pHead6, 3); PrintNodeProntToTail(pHead6); printf("\n"); PrintNodeProntToTail(pHead6); printf("删除非尾节点 3 :\n"); pDelNode = FindDataInNodeList(pHead6, 3); DeleteNotTailNode(pDelNode); PrintNodeProntToTail(pHead6); printf("\n非头节点 5 前的插入:\n"); PrintNodeProntToTail(pHead6); pInsertNode = FindDataInNodeList(pHead6, 5); InsertNotHead(pInsertNode, 99); PrintNodeProntToTail(pHead6); printf("\n");}int main(){ test(); return 0;}]]></content>
<categories>
<category>编程之美</category>
</categories>
<tags>
<tag>链表面试题</tag>
<tag>编程之美</tag>
<tag>无头单链表</tag>
</tags>
</entry>
<entry>
<title><![CDATA[C语言版通讯录]]></title>
<url>%2F2018%2F06%2F17%2FC%E8%AF%AD%E8%A8%80%E5%AE%9E%E7%8E%B0%E4%B8%80%E8%88%AC%E9%80%9A%E8%AE%AF%E5%BD%95%2F</url>
<content type="text"><![CDATA[写在前面 C语言通讯录可以用来存储1000个人的信息。 每个人的信息包括姓名、年龄、性别、电话、住址。 实现基本的增、删、查、改、排序、打印、清空基本功能。 思路分析: 首先我们可以分三个模块来解决这个问题,第一个模块我们需要一个头文件,这个头文件里可以包含一些相应信息,当实现文件和测试文件包含自己定义的头文件时便可以获得一些相关的信息。所以头文件里应该包括一个结构体,这个结构体里应包含姓名,性别,年龄,电话,住址。同时还可以定义一个结构体,这个结构体里包含通讯录,同时通讯录里人员的计数变量,将通讯录的地址传到别的地方便可以实现对它遍历或者其他操作。 第二个模块便是我们的测试函数,测试函数便可以实现我们的菜单打印,同时由我们接收不同的值便可以实现不同的操作,就是相应的方法的实现,这里很明显可以通过一个switch语句来控制功能选择,用do-while语句来控制重复选择的循环部分。 第三个模块便是我们的方法实现的函数,将模块2里定义的类型为通讯录的地址传到各个方法里,这样便可以实现对通讯录的操作。 代码实现头文件: <contect.h>12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849#pragma once;#include<iostream>#include<Windows.h>#include<cstdio>#define NAME_MAX 20#define TEL_MAX 12#define SEX_MAX 4#define MAX 1000using namespace std;enum{ Exit, Add, Del, Search, Modify, Empty, Sort, Show};struct Stu{ char Name[NAME_MAX]; int Age; char Tel[TEL_MAX]; char Sex[SEX_MAX]; char Address[50];};typedef struct Stu Stu;typedef struct Contact{ Stu Con[MAX]; //容量 int sz; //计数}Contact, *pContact;//函数声明void InitContact(pContact pCon); //初始化void AddContact(pContact pCon); //新建void ShowContact(pContact pCon); //打印void DelContact(pContact pCon); //删除void SearchContact(pContact pCon); //查找void ModifyContact(pContact pCon); //修改void EmptyContact(pContact pCon); //清空void SortContact(pContact pCon); //排序 函数实现: contect.cpp123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175#define _CRT_SECURE_NO_WARNINGS 1#include"contect.h"void InitContact(pContact pCon){ pCon->sz = 0; memset(pCon->Con, 0, MAX * sizeof(Stu));}void AddContact(pContact pCon){ cout<<"请输入联系人姓名:"; cin >> pCon->Con[pCon->sz].Name; cout << "请输入联系人年龄:"; cin >> pCon->Con[pCon->sz].Age; cout << "请输入联系人电话:"; cin >> pCon->Con[pCon->sz].Tel; cout << "请输入联系人性别:"; cin >> pCon->Con[pCon->sz].Sex; cout << "请输入联系人地址:"; cin >> pCon->Con[pCon->sz].Address; cout << "═════联系人 " << pCon->Con[pCon->sz].Name << " 的信息保存成功════" << endl << endl; pCon->sz++;}void ShowContact(pContact pCon){ cout << endl; cout << "╔═════════════════════════════════════════════════════════════╗" << endl; printf("%3s\t%6s\t%2s%16s%8s%20s\n","║序号","姓名","年龄","电话","性别","地址║"); for (int i = 0; i < pCon->sz; i++) { printf("%3d\t%6s\t%2d%16s%8s%20s\n", i + 1, pCon->Con[i].Name, pCon->Con[i].Age, pCon->Con[i].Tel, pCon->Con[i].Sex, pCon->Con[i].Address); } cout << "╚═════════════════════════════════════════════════════════════╝" << endl; cout << "温馨提示:输出完成!!!" << endl << endl;}int FindInContact(pContact pCon, char *ch){ for (int i = 0; i < pCon->sz; i++) { if (strcmp(pCon->Con[i].Name,ch) == 0) { return i; } } return -1;}void DelContact(pContact pCon){ char ch[20]; ShowContact(pCon); cout << "请输入上面通讯录中要删除联系人的姓名:"; scanf("%s", &ch); int NUM = FindInContact(pCon, ch); if (NULL == -1) { printf("通讯录中没有该联系人!!!"); } if (NUM <= pCon->sz && NUM != -1) { for (int j = NUM; j <= pCon->sz-1; j++) { pCon->sz--; pCon->Con[j] = pCon->Con[j + 1]; printf("删除成功!!!\n"); } }}void SearchContact(pContact pCon){ char ch[20]; printf("请输出你要查找联系人的姓名:"); scanf("%s", &ch); int NUM = FindInContact(pCon, ch); if (NUM != -1) { cout << "╔═════════════════════════════════════════════════════════════╗" << endl; printf("%3s\t%6s\t%2s%16s%8s%20s\n", "║序号", "姓名", "年龄", "电话", "性别", "地址║"); printf("%3d\t%6s\t%2d%16s%8s%20s\n", NUM + 1, pCon->Con[NUM].Name, pCon->Con[NUM].Age, pCon->Con[NUM].Tel, pCon->Con[NUM].Sex, pCon->Con[NUM].Address); cout << "╚═════════════════════════════════════════════════════════════╝" << endl; } else printf("你要找的联系人不存在\n");}void ModifyContact(pContact pCon){ char ch[NAME_MAX]; printf("请输入要修改的联系人的姓名:"); scanf("%s", &ch); int NUM = FindInContact(pCon, ch); if (NUM != -1) { cout << "╔═════════════════════════════════════════════════════════════╗" << endl; printf("%3s\t%6s\t%2s%16s%8s%20s\n", "║序号", "姓名", "年龄", "电话", "性别", "地址║"); printf("%3d\t%6s\t%2d%16s%8s%20s\n", NUM, pCon->Con[NUM].Name, pCon->Con[NUM].Age, pCon->Con[NUM].Tel, pCon->Con[NUM].Sex, pCon->Con[NUM].Address); cout << "╚═════════════════════════════════════════════════════════════╝" << endl; printf("请把姓名修改成:"); scanf("%s", pCon->Con[NUM].Name); printf("请把年龄修改成:"); scanf("%d", &pCon->Con[NUM].Age); printf("请把电话修改成:"); scanf("%s", pCon->Con[NUM].Tel); printf("请把性别修改成:"); scanf("%s", pCon->Con[NUM].Sex); printf("请把地址修改成:"); scanf("%s", pCon->Con[NUM].Address); }}void EmptyContact(pContact pCon){ pCon->sz = 0; printf("清空成功!!!\n\n");}void SortContact(pContact pCon){ for (int i = 0; i < pCon->sz; i++) { for (int j = 0; j < pCon->sz-i-1; j++) { if (strcmp(pCon->Con[j].Name, pCon->Con[j + 1].Name)>0) { Stu temp; temp = pCon->Con[j]; pCon->Con[j] = pCon->Con[j + 1]; pCon->Con[j + 1] = temp; } } }} 测试函数: test.cpp12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273#include"contect.h"void menu(){ cout << "╔═══════════════════════════════════════════════════╗" << endl; cout << "║═══════════════════ 通讯录 ══════════════════════║" << endl; cout << "║═══ ═══║" << endl; cout << "║═══ 1.新建 2.删除 ═══║" << endl; cout << "║═══ 3.查找 4.修改 ═══║" << endl; cout << "║═══ 5.清空 6.排序 ═══║" << endl; cout << "║═══ 7.打印 0.退出 ═══║" << endl; cout << "║═══ ═══║" << endl; cout << "╚═══════════════════════════════════════════════════╝" << endl;}void test(){ system("color c"); //改变背景 Contact my_con; InitContact(&my_con); int input = 0; do { menu(); cout << "请选择:"; cin >> input; switch (input) { case Exit: break; case Add: AddContact(&my_con); break; case Del: DelContact(&my_con); break; case Search: SearchContact(&my_con); break; case Modify: ModifyContact(&my_con); break; case Empty: EmptyContact(&my_con); break; case Sort: SortContact(&my_con); break; case Show: ShowContact(&my_con); break; default: cout << "选择错误:->!!!" << endl; break; } } while (input);}int main(){ test(); return 0;} 输出 问题思考1、改写成链表存储,不同存储结构的区别? 2、动态版通讯录、文件版通讯录设计? 3、编写大型通讯录系统,将通讯录数据存储在数据库中?]]></content>
<categories>
<category>小项目</category>
</categories>
<tags>
<tag>C语言版通讯录</tag>
<tag>结构体存储通讯录</tag>
</tags>
</entry>
<entry>
<title><![CDATA[TCP建立连接和关闭连接详解(三次握手&四次挥手&面试问题)]]></title>
<url>%2F2018%2F06%2F15%2FTCP%E5%BB%BA%E7%AB%8B%E5%92%8C%E6%96%AD%E5%BC%80%E8%BF%9E%E6%8E%A5%E8%BF%87%E7%A8%8B%EF%BC%88%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B%26%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B%EF%BC%89%2F</url>
<content type="text"><![CDATA[写在前面 三次握手、四次挥手问题是面试中一定会问到的,包括在《高质量C/C++编程》一书中也有类似的习题。这篇blog就TCP连接的建立和关闭过程及相关面试问题做一个详解,如有错误,望指正。 如果对TCP还不了解,可以先阅读这篇文章: TCP固定头部结构详解 三次握手: 三次握手是就是TCP在建立连接时的过程。 TCP是主机对主机层的传输控制协议,提供可靠的连接服务,采用三次握手确认建立一个连接。 A主机运行TCP客户端程序,B主机运行TCP服务器端程序,在A主机发送SYN之前,客户端和主机都处于CLOSED状态。 B主机先创建传输控制块PCB。准备接收客户端的请求,然后服务器端进入LISTEN状态,等待客户端连接请求。 第一次握手 TCP客户端先创建传输控制块PCB,然后向服务器B发送连接请求报文段,此时首部中的同步位SYN置位1,同时选择一个初始序号seq = x,这是TCP客户端进入SYN-SENT(同步已发送)状态。 第二次握手 服务器B收到A发送的连接请求报文段后,如果同意连接,则向A发送确认,在确认报文段中,将ACK和SYN都置为1,确认号是ack = x + 1,同时也为自己选择一个初始序号seq = y,这是TCP服务器进入SYN-RCVD(同步收到)状态。 第三次握手 TCP客户端收到B发送的确认后,还要向B给出确认,确认报文ACK置为1,确认号ack = y + 1,自己的序号为seq = x + 1。TCP协议规定,ACK报文段可以携带数据,如果不携带数据,则不消耗序号,则下一数据报文段的序号仍然为seq = x + 1,这时TCP已经建立连接,A进入ESTABLISHED(已建立连接状态)。当B收到A的确认后也进入ESTANLISHED状态。 上面这种建立连接的过程称为三次握手。 【面试问题1】为什么A还要再发送一次确认呢? 这是为了防止某些失效的连接请求报文再次传送给服务器B而导致出错。失效连接报文来源?假如TCP连接就是两次握手,如果说客户端A给服务器端B发送了连接请求报文段,但是报文段丢失,导致B没有收到,于是A重传连接请求,第二次的服务器端B收到了连接请求,建立了连接。数据传输完成后就释放连接。在这个过程中,客户端A共发送两次连接请求报文,一个报文丢失,另一个报文到达了B。不存在“已失效的连接报文”。 现在考虑另一种特诉情况,主机A第一次发送的连接请求并没有丢失,而是因为网络节点导致延迟达到主机B,主机B以为是主机A又发起的新连接,于是主机B同意连接,并向主机A发回确认,但是此时主机A根本不会理会,主机B就一直在等待主机A发送数据,导致主机B的资源浪费。 四次挥手: 四次挥手是TCP释放连接的过程,具体过程请看下图分析。 完成数据传输后,通信的双方都可以释放连接,主机A、B先都处于ESTABLISHED状态。 第一次挥手 A的应用进程先向其TCP发送连接释放报文段,A停止再发送数据,主动关闭TCP连接,A把连接释放报文段首部的FIN置1,其序号seq = u,它等于前面已经传送过的数据的最后一个字节序号加1。这时A进入FIN-WAIT-1(终止等待1)状态,等待B的确认,注意:TCP规定,FIN报文段即使不携带数据也要消耗一个序号。 第二次挥手 B收到连接释放报文段后,立即发出确认,确认号是ask = u + 1,而这个报文段自己的序号是v,等于B前面已传送过的数据的最后一个字节的序号加1。然后B就进入CLOSE-WAIT(关闭等待)状态,TCP服务器进程这时应通知高层应用进程,因而从A到B这个方向的连接就释放了,这时的TCP处于半关闭状态,即A已经没有数据要发送了,但B若要发送数据,A仍要接收,也就是说,从B到A的方向连接并灭有关闭。这个状态可能会持续一些时间。 第三次挥手 A收到来自B的确认后,就进入FIN-WAIT-2(终止等待2)状态,等待B发出的连接请求释放报文段。 若B已经没有要向A发送的数据,其应用进程就通知TCP释放连接。这时B发出的连接释放报文段必须使FIN = 1,现假定B的序号为w(在半关闭状态B可能又发送了一些数据)。B还必须重复上次已发送过的确认号akc = u + 1。这时B就进入LAST-ACK(最后确认)状态,等待A的确认。 第四次挥手 A在收到B的连接释放报文段后,必须对此发出确认。在确认报文段中把ACK置1,确认号ack = w + 1,而自己的序号是seq = u + 1(根据TCP标准,前面发送的FIN报文段要消耗一个序号)。然后进入到TIME-WAIT(时间等待)状态。请注意,现在TCP连接连接还没有释放掉。必须经过时间等待计时器(TIME-WAIT)设置的时间2MSL后,A才进入到CLOSED状态。时间MSL叫做最长报文段寿命。MSL是根据工程来设置时长的,假设为2min。因此从A进入到TIME-WAIT状态后,要经过4分钟才能进入到CLOSED状态,才能开始建立下一个新的连接。当A撤销相应的传输控制块TCB后,就结束了这次的TCP连接。 【面试问题2】为什么A在TIME-WAIT状态必须等待2MSL的时间呢? 这有两个原因:第一:为了保证A发送的最后一个ACK报文段能够到达B,这个报文段有可能丢失,因而使处在LAST-WAIT的状态B收不到对方已发送的FIN-ACK报文段的确认,B会超时重传这个FIN + ACK报文段,而A就能在2MSL时间内接收到这个重传的FIN + ACK报文段,接着A重传一次确认,重新启动2MSL计时器。最后,A和B都正常进入到CLOSED状态。如果A在TINE-WAIT装态不等待一段时间,而是在发送完ACK报文段后立即释放连接,那么久无法收到B重传的FIN + ACK报文段,因而也不会再发送一次确认报文段。这样,B就无法按正常步骤进入CLOSED状态。第二:防止上文提到的“已失效的连接请求报文”出现在本连接中。A发送完最后一个ACK报文段后,再经过时间2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样可以使下一个新的连接中不会出现这种旧的连接请求报文段。B只要收到A发出的确认,就进入CLOSED状态,同样,B在撤销相应的传输控制块TCB后,就结束了这次的TCP连接,我们注意到,B结束TCP连接的时间要比A早一些。]]></content>
<categories>
<category>SO Kernals</category>
</categories>
<tags>
<tag>ACK机制</tag>
<tag>TCP连接过程</tag>
<tag>三次握手四次挥手</tag>
</tags>
</entry>
<entry>
<title><![CDATA[世界时间(基于HTTP服务器框架实现)]]></title>
<url>%2F2018%2F06%2F10%2Fweb%2F</url>
<content type="text"><![CDATA[基于HTTP服务器框架实现世界时间查看的功能,支持客户/服务器模式,客户只需传送请求方法和路径给服务器,服务器根据页面生成规则,将资源以HTML页面形式返回给客户端。 客户端发起请求,服务器创建多线程,读取并解析请求,服务器根据请求特征决定生成静态页面或者动态页面的响应,并能够进行差错处理,如果请求资源不存在,服务器打印日志。 世界时间查询页面的响应利用CGI技术,具有较好的解耦合特点,各地时间和北京时间的时差存放在MySQL数据库中。 使用WebBench对服务器进行压力测试。 详细特点1、设计思路清晰: 这是一个基于HTTP服务器框架的网站,实现了世界时间查看、加法计算的功能。每个功能都是一组CGI程序,这样设计,可以实现较好的解耦合,技术支持(HTTP框架)与具体业务无关。 2、简单快速: HTTP框架是基于TCP实现的,客户向服务器发起请求时,只需要发送方法名和路径。请求方法支持GET和POST方法,每种方法规定了客户与服务器联系的类型不同。由于HTTP协议比较简单,使得项目的程序规模不大,因此通信速度较快。 3、灵活: HTTP允许任意类型的数据对象传输,传输类型可以用Content-Type加以标记。 4、无连接: 限制每次连接只处理一个请求,服务器处理完客户端请求,并收到客户端的应答后,就断开连接。 5、无状态: 主要是因为HTTP协议是无状态的。协议对事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,就必须要重传。缺点就是:可能导致每次连接传送的数据量增大。 设计思路客户端/服务器之间数据传输 基本流程 1、服务器启动 2、进入事件循环 (1)创建线程:一个线程处理一个客户端。 (2)读取并解析请求:读取首行、Header、Body。 (3)根据读取的请求特征决定,按什么样的规则来生成响应页面。 如果是静态页面(GET请求无参数):将服务器磁盘上的本地文件返回给浏览器。 如果是动态页面(GET请求,有query-string参数;POST请求):调用对应的CGI程序,让CGI生成动态页面。 静态文件的不同类型处理: HTTP响应加一个Content-Type,告诉系统这是一个HTML,UTF-8编码格式。 注意使用POST这种方法传输数据时,HTTP在数据发送完后,并不会发送相应的数据传输完毕提示信息,所以HTTP服务器提供了另一个环境变量CONTENET_LENGTH,该环境变量记录了传输过来了多少个字节长度的数据(单位为字节),所以在编写CGI程序时,如果method为POST,就需要通过该变量来限定读取的数据的长度。 CGI模式 CGI是一种标准,规定动态页面生成的标准。 特点: 解耦合 根据不同的业务需求,实现一组CGI程序,放到某个具体目录中,请求如果触发了CGI的流程(动态页面:GET请求,带有query-string; POST请求)。 1、创建一对管道 创建一对管道,实现全双工通信,供父子进程间交换数据,读写操作。 2、创建子进程 对于父进程: (1)将请求相关的内容写到管道中; (2)尝试从管道中读取输出; (3)把结果写回到Socket里面,并回收子进程。 对于子进程: (1)设置环境变量(METHOD、QUERY-STRING、CONTENT-LENGTH); (2)重定向:把标准输入、标准输出重定向到管道中,完成父子进程间通信; (3)子进程程序替换。 简单并发测试 页面URL 并发数 时间/sec pages/min bytes/sec 成功 失败 性能分析 主页:/index.html 100 15 2640 87429 660 0 主页:/index.html 100 1 4140 141905 69 0 主页:index.html 10000 2 0 0 0 0 fork failed 世界时间:/wordtime/index.html 100 15 1892 95729 473 0 世界时间:/wordtime/index.html 100 1 3179 159319 53 0 世界时间:/wordtime/index.html 1000 1 2819 154429 47 0 加法计算器:/cgi-bin/index.html 100 15 1400 103495 350 0 加法计算器:/cgi-bin/index.html 100 1 2039 161368 34 0 加法计算器/cgi-bin/index.html 1000 1 2280 162920 38 0 文件目录 问题1、运行cgi模式时,每次提交数据并进行submit后都会自动出现提醒下载的页面 原因:在响应报头中,将Content-Type中的”text”写成”test”。而浏览器对于不能识别或解析的实体,都会提醒用户下载。 2、本地环回测试ok,Linux下的浏览器测试也可以,但不能接外部的浏览器访问(没有设置桥接模式) 嗯~要是在外部浏览器测试的话千万别忘记关闭防火墙 项目展示 http://118.24.76.81:9090/]]></content>
<categories>
<category>小项目</category>
</categories>
<tags>
<tag>世界时间</tag>
<tag>HTTP协议</tag>
</tags>
</entry>
<entry>
<title><![CDATA[保证TCP协议的可靠传输]]></title>
<url>%2F2018%2F05%2F31%2FTCp%E5%8D%8F%E8%AE%AE%E5%8F%AF%E9%9D%A0%E4%BC%A0%E8%BE%93%2F</url>
<content type="text"><![CDATA[写在前面 TCP协议在网络传输中保证了其可靠性,本篇文章对TCP协议可靠传输机制做了详细介绍。 介绍:(ACK机制&滑动窗口&拥塞控制&流量控制)。 回顾内容 传输层使用的两个主要协议:TCP和UDP TCP 主要特性有特性有以下几点: (1)面向连接,在数据传送前必须建立连接,在数据传送结束后必须释放连接。在一个TCP连接中,仅有两方进行彼此通信。广播和多播不能用于TCP。 (2)点对点,每一条TCP连接只能有两个端点。从socket角度来说,通信双方需要建立套接字,套接字由IP地址和端口号组成,数据到达传输层之后会被送到端口对应的应用程序。 (3)提供可靠交付服务。 (4)支持全双工通信。 (5)面向字节流。面向报文的传输方式是应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。因此,应用程序必须选择合适大小的报文。若报文太长,则IP层需要分片,降低效率。若太短,会是IP太小。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。这也就是说,应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。面向字节流的话,虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序数据看成是一连串的无结构的字节流。TCP有一个缓冲,当应用程序传送的数据块太长,TCP就可以把它划分短一些再传送。如果应用程序一次只发送一个字节,TCP也可以等待积累有足够多的字节后再构成报文段发送出去。 总述1、确认应答(ACK)机制: 2、超时重传机制: 接收方收到报文就会确认,发送方发送一段时间后没有收到确认就重传。 3、滑动窗口机制: 4、数据校验: TCP将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。 5、数据合理分片和排序: UDP:IP数据报大于1500字节,大于MTU.这个时候发送方IP层就需要分片.把数据报分成若干片,使每一片都小于MTU.而接收方IP层则需要进行数据报的重组.这样就会多做许多事情,而更严重的是,由于UDP的特性,当某一片数据传送中丢失时,接收方便无法重组数据报.将导致丢弃整个UDP数据报. TCP会按MTU合理分片,接收方会缓存未按序到达的数据,重新排序后再交给应用层。 5、流量控制: 当接收方来不及处理发送方的数据,就提示发送方降低发送的速率,防止包丢失。 7、拥塞控制: 当网络拥塞时,减少数据的发送。 上面简单的讲了TCP保证可靠传输的几个机制,下面对较难理解的面深入探究。 滑动窗口滑动窗口是干什么的?有一种机制叫做ACK确认应答【过程如图】,收到数据段,给发送一个ACK确认应答,收到ACK应答后,再发送数据段,明显会降低效率,如果数据往返时间较长时,会影响到性能。 上面这种方式浪费效率,如果换种方式发送呢 ^_^ 一次发送多个数据段,统一确认后,再发送多个数据段。 上面这种传送机制就是窗口机制。 窗口是什么? 窗口大小:就是无需等到确认应答而可以继续发送数据的最大值。(上图窗口就是4000字节,也就是四个段) 传输过程: 1、客户端和服务器端各自建立套接字,通过彼此的套接字进行通信; 2、服务器端绑定监听端口,然后监听,循环等待来自客户端的连接; 3、一旦收到来自客户端的连接,进行三次握手,一旦连成功就fork()一个子进程来处理和当前客户端的连接,然后父进程继续监听客户端的连接; 4、发送前面四个段,无需ACK确认应答,直接发送; 5、收到第一个ACK后,滑动窗口向后移动,发送第五个数据段,以此类推; 6、操作系统为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有那些数据没有应答,只有确认应答过的数据,才能从缓冲区删掉; 7、一旦数据传输完毕就是放连接。 就是说随着时间推移,滑动窗口也在推移,滑动窗口的会变化,内部缓存数据不停的更新,根据网络的拥塞情况,发送端可以调控滑动窗口的大小来控制流量 ,滑动窗口就是一个滑的过程啊~ ^* ~ ^* 可能解释的有点啰嗦了 4、窗口越大,网络吞吐量就越高。 吞吐量就是单位时间内通过某个网络(信道、接口)的数据量。 接收窗口大小取决于应用(比如说tomcat:8080端口的监听进程)、系统、硬件的限制。 流量控制简单来说就是接收方处理不过来的时候,就把窗口缩小,并把窗口值告诉发送端。 当窗口值为0,而接受方把窗口值恢复(比如ACK=1,ack=601,rwnd=200),但确认丢失,进入相互等待的死锁局面。所以如果窗口值为0,发送端就会开启一个持续计数器,每个一段时间询问一下接收方。 拥塞控制 什么是拥塞? 是指计算机网络中,某一个时段,某一资源的需求量超过了该资源可提供的可用部分,网络性能变差。 什么是拥塞控制? 所谓的拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或者节点不至于过载,拥塞控制是一个全局的过程。 几种拥塞控制的方法:因特网建议标准RFC定义了几种拥塞控制的算法; 满开始,拥塞避免, 快重传,快恢复。 拥塞控制描述: 唯一的方法就是尝试各种不同的发送速度。比如一开始以 100kb/s 的速率发送数据,如果没问题,再将速率提高到 200kb/s,再没问题继续提升发送速率。一旦达到某个上限后,便开始出现丢包现象,发送方就可以认为,网络已经拥塞了,于是降低发送速率,减轻网络负担。 控制流程简述: 慢开始、拥塞避免 发送方维持一个拥塞窗口的状态变量,其大小取决于网络的拥塞程度,动态地变化,而发送窗口一般取拥塞窗口和对方给出的接收窗口的最小值(为了便于描述,后面的分析中假定对方给出的接收窗口足够大,这样将发送窗口等于拥塞窗口就可以了)。 慢开始算法的核心是从小到大逐渐增大发送窗口,也就是说,从小到大逐渐增大拥塞窗口的数值。通常在刚开始发送报文段时,先把拥塞窗口设置为一个最大报文段MSS的数值,而在每收到对上一轮报文段(,每次加倍后的报文段的个数,可能不止一个报文段)的确认后,就把拥塞窗口的数值加倍。 为了防止拥塞窗口增长过大引起网络拥塞,还需要维护一个慢开始门限的状态变量,当拥塞窗口的值小于慢开始门限时,使用慢开始算法,一旦拥塞窗口的值大于慢开始门限的值,就改用拥塞避免算法。 拥塞避免算法的思路是让拥塞窗口缓慢地增大,收到每一轮的确认后,将拥塞窗口的值加1,而不是加倍,这样拥塞窗口的值按照线性规律缓慢增长。 无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(没有按时收到确认),就把慢开始门限设置为出现拥塞时发送窗口值的一般,但最小不能小于2个MSS值,而后把拥塞窗口的值重新设置为1个MSS,执行慢开始算法。 快重传,快恢复 快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认(重复发送对前面有序部分的确认),而不是等待自己发送数据时才进行稍待确认,也不是累积收到的报文发送累积确认,如果发送方连续收到三个重复确认,就应该立即重传对方未收到的报文段(有收到重复确认,说明后面的报文段都送达了,只有中间丢失的报文段没送达)。 快恢复算法与快重传算法配合使用,其过程有如下两个要点: 1、当发送方连续收到三个重复确认时,就把慢开始门限减半,这是为了预防网络发生拥塞。注意,接下来不执行慢开始算法。 2、由于发送方现在认为网络很很可能没有发生特别严重的阻塞(如果发生了严重阻塞的话,就不会一连有好几个报文段到达接收方,就不会导致接收方连续发送重复确认),因此与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口的值不设为1个MSSS),而是把拥塞窗口的值设为慢开始门限减半后的值,而后开始执行拥塞避免算法,线性地增大拥塞窗口。 问题思考1、网络拥堵是怎么来的? 2、RFC 定义的 4 种拥塞控制算法是什么?分别讲述流程以及原理? 3、为什么TCP传输这么复杂? 因为要保证可靠传输,同时又要尽可能提高性能。]]></content>
<categories>
<category>SO Kernal</category>
</categories>
<tags>
<tag>TCP协议</tag>
<tag>ACK机制</tag>
<tag>滑动窗口</tag>
<tag>拥塞控制</tag>
<tag>流量控制</tag>
</tags>
</entry>
<entry>
<title><![CDATA[UDP协议实现服务器-客户端通信]]></title>
<url>%2F2018%2F05%2F22%2F%E5%9F%BA%E4%BA%8EUDP%E5%8D%8F%E8%AE%AE%E7%9A%84%E5%9B%9E%E6%98%BE%E6%9C%8D%E5%8A%A1%E5%99%A8%2F</url>
<content type="text"><![CDATA[写在前面 回显服务器的代码实现过程,文章加入了详细的设计过程以及其他的计算机网络理论知识。 操作环境:CentOS 7.0 X64 操作系统、Vim编辑器、Gcc编译器。 预备知识 本文的目的是写回显服务器,在撸代码之前,先来个热身。 IP地址是什么IP地址有IPV4、IPV6之分,一般不特俗说明,默认就是IPV4。 IP地址是用来标识不同的主机,每个主机都有唯一的IP地址;对于IP4来说,IP地址是一个4字节,32位整数;IP地址用“点分”制表示,如:192.168.1.11(用点分割的每个数范围0~255)。 源IP地址&目的IP地址 很容易理解,都是地址,和寄快递的收发地址一样,从上海发往西安的快递,源IP就是上海,目的IP就是西安。 端口号是什么 端口号是传输层协议内容 端口号是2字节196位整数;端口号用来标识一个进程,告诉操作系统,当前数据要交给哪一个进程来处理;一个进程可以绑定多个端口号,但是一个端口号不可以绑定多个进程。 源端口号&目的端口号 在源IP&目的IP中,我们用的寄快递的例子帮助理解,在这里,还是用发快递帮助理解。源IP与目的IP标识了发件人地址和收件人的地址,地址有了,那么包裹就会交给快递员运送每个,快递员都有一个工号,工号是唯一的。这就对应了数据传输过程中,由哪个进程来处理数据。再来到寄快递问题上,有的快递包裹比较大,这就要多个快递员来运输,那么一个包裹由多个快递员运输,记在物流信息上就是这样的格式:一个包裹的目的地 + 多个快递员工号;这家公司接的都是大包裹,一个快递员只能送一个包裹。对应到网络传输中,就是一个进程可以绑定多个端口号,但一个端口号不可以绑定多个进程。 函数介绍 为后面撸代码介绍几个函数。 网络字节序:在C语言中我们知道,内存中的数据存储有大小端之分;数据在磁盘中存储也有大小端之分,在这里我还想啰嗦一个C语言问题,怎样判断自己的计算机内存是大端字节序还是小端字节序存储方式?[假装思索……] 大小端判断很有可能成为你将来的面试题。以前我总结过这样的问题。 以后再附上链接:哈哈 (只想引入下面一句话)在网络数据流中同样有大小端之分,那么如何定义网络数据流的地址呢? 【看图理解】 【看图说话】 如果发送主机是小端,就要准换成大端再发送,如果是大端,直接发送即可。 为了使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。 12345#include<arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); 调用函数就能解决存储字节序不统一的问题 socket编程常见API:这部分只把函数列出来,详细介绍请戳作者下面博客: 请戳: socket套接字总结 123456789//创建socket文件描述符 (TCP/UDP,客户端+服务器)int socket(int domain, int type, int protocol);ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);//关闭套接字int close(int fd); sockaddr结构: socket API是一 层抽象的网络编程接口 ,适用于各种底层网络协议,如IPv4、IPv6.然而, 各种网络协议的地址格式各不相同。 注意:socket API可以都用 struct sockaddr *类型表 , 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。 UDP协议 通过上面的学习对UDP有个直观的认识,再详细讨论以下几点: 无连接 知道目的端的IP和端口号就能传输,不需要建立连接。 不可靠传输 没有确认机制,没有重传机制,如果因为网络故障无法发送到对方,UDP协议层也不会给应用层返回任何错误信息。 面向数据报 不能够灵活的控制读写数据的次数和数量。 以上几点在代码中还能得到学习和理解。 服务器和客户端是怎么运行起来的?先说说什么是回显服务器: 回显服务器是最简单的服务器,客户端发起请求,服务器收到请求,然后客户端发送什么内容,服务器就给客户端返回什么内容。 服务器: 1、创建socket 2、绑定端口 3、循环的读取数据 4、针对读取到的数据进行计算和处理 5、把处理后的结果发回客户端 客户端: 1、创建socket文件 2、给服务器发送请求 3、从服务器中读取结果。 有了步骤,实现起来就只需要把步骤翻译成Code了: 服务器实现123456789101112131415161718192021222324252627282930313233343536373839#include<stdio.h>#include<unistd.h>#include<string.h>#include<netinet/in.h>#include<arpa/inet.h>#include<sys/socket.h>int main(){ //创建一个套接字,并检测是否创建成功 int sockSer = socket(AF_INET, SOCK_DGRAM, 0); if(sockSer == -1) perror("socket"); struct sockaddr_in addrSer; //创建一个记录地址信息的结构体 addrSer.sin_family = AF_INET; //使用AF_INET协议族 addrSer.sin_port = htons(5050); //设置地址结构体中的端口号 addrSer.sin_addr.s_addr = inet_addr("192.168.3.169"); //设置通信ip //将套接字地址与所创建的套接字号联系起来,并检测是否绑定成功 socklen_t addrlen = sizeof(struct sockaddr); int res = bind(sockSer,(struct sockaddr*)&addrSer, addrlen); if(res == -1) perror("bind"); char sendbuf[256]; //申请一个发送数据缓存区 char recvbuf[256]; //申请一个接收数据缓存区 struct sockaddr_in addrCli; while(1) //服务器一直循环接受客户端的请求 { recvfrom(sockSer,recvbuf,256,0,(struct sockaddr*)&addrCli, &addrlen); //从指定地址接收客户端数据 printf("Cli:>%s\n",recvbuf); printf("Ser:>"); scanf("%s",sendbuf); sendto(sockSer,sendbuf,strlen(sendbuf)+1,0,(struct sockaddr*)&addrCli, addrlen); //向客户端发送数据 } return 0;} 客户端实现1234567891011121314151617181920212223242526272829303132333435#include<stdio.h>#include<unistd.h>#include<string.h>#include<netinet/in.h>#include<arpa/inet.h>#include<sys/socket.h>int main(){ //创建一个套接字,并检测是否创建成功 int sockCli = socket(AF_INET, SOCK_DGRAM, 0); if(sockCli == -1){ perror("socket"); } addrSer.sin_family = AF_INET; //使用AF_INET协议族 addrSer.sin_port = htons(5050); //设置地址结构体中的端口号 addrSer.sin_addr.s_addr = inet_addr("192.168.3.169"); //设置通信ip socklen_t addrlen = sizeof(struct sockaddr); char sendbuf[256]; //申请一个发送数据缓存区 char recvbuf[256]; //申请一个接收数据缓存区 while(1){ //向客户端发送数据 printf("Cli:>"); scanf("%s",sendbuf); sendto(sockCli, sendbuf, strlen(sendbuf)+1, 0, (struct sockaddr*)&addrSer, addrlen); 接收来自客户端的数据 recvfrom(sockCli, recvbuf, BUFFER_SIZE, 0, (struct sockaddr*)&addrSer, &addrlen); printf("Ser:>%s\n", recvbuf); } return 0;} 问题思考1、UDP协议的特点以及和TCP协议的区别? 2、UDP连接建立过程分析、TCP呢? 3、UDP协议是不可靠的,为什么还要使用?]]></content>
<categories>
<category>SO Kernal</category>
</categories>
<tags>
<tag>计算机网络</tag>
<tag>UDP协议</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hexo + GitHub + Blog【版本:小猪佩奇】]]></title>
<url>%2F2018%2F05%2F20%2FHexo%2BGitHub%20Blog%2F</url>
<content type="text"><![CDATA[写在前面 利用周末时间搭建了Hexo + github + 个性化域名博客。 通过不断地测试和完善,实现了常用功能以及基本的页面美化。 问题概况 在搭建过程中遇到了很多棘手的问题,在中途因为没有测试,还崩溃过,只能归零,从头再来。 非常感谢众多大神的博客,给了我很多帮助,推荐几个程序员技术交流网站/社区:知乎、简书、CSDN、掘金、牛客、GitHub。 因为有了第一次的经验和教训,所以第二次从环境搭建到站点/主题配置就一气呵成了。 我的博客后期用来管理优质文章、自己写的小项目、分享我喜欢的图片、音乐歌单、笔记等等,欢迎读者收藏。 工具推荐1、Atom 2、Sublime Text 3 3、NotePad ++ 4、冰点文库下载器 5、LastActivity View 6、Xshell 7、Xftp 6 8、Mind Master 9、Fiddler 10、VC++ 6.0 MSDN 11、Typora]]></content>
<categories>
<category>工具操作</category>
</categories>
<tags>
<tag>Git</tag>
<tag>技术</tag>
</tags>
</entry>
<entry>
<title><![CDATA[文件压缩(基于Huffman树实现无损压缩)]]></title>
<url>%2F2018%2F04%2F20%2Ffile_compress%2F</url>
<content type="text"><![CDATA[写在前面 项目采用Huffman编码的方式进行文件压缩与解压缩。主要原理是通过Huffman编码来表示字符,出现次数多的编码短,出现次数少的编码长,这样整体而言,所需要的bit位是减少的,就实现了文件压缩功能。 读取文件中的字符出现次数,构建Huffman树,然后解析这个字符的每一位,遇到一个叶子结点,就代表还原了一个字符,这时就将这个字符写到解压缩文件里。 注意当大部分字符出现的频率都差不多时,Huffman压缩的效率比较低。 项目源码待更新 需求分析 输入:文本文件(压缩文件) 输出:压缩文件/解压文件(文本文件) (压缩时间) 知识点:堆、霍夫曼树、二叉树遍历、存储数据结构设计、文件流操作、字符汉字编码方式、二进制文件读写、优先级队列。 模块及框架设计主要模块: 1、压缩模块 2、解压模块 辅助模块: 1、字符识别及权重获取 2、Huffman树构造 3、获取huffman编码 压缩步骤1、统计字符出现的次数 因为文件底层都是有256个字符存储的,所以使用一个256的数组来统计字符出现的次数。在项目中,使用了一个结构体,将次数、字符、huffman编码对应起来。因为不管是文件、图片、音频、视频。他们的底层都是以字符形式存储的,读取的时候就按照字符格式读取。 2、构建huffman树 采用Huffman编码,将一组集合中权值最小的两棵树拿出来构建一棵树,选择权值最小的两棵树拿出来构建一棵树,再将权值之和作为节点插入到这个集合中,不断重复,直到集合中只有一个树,寻找最小的两棵树,利用了priority_queue,在这里将字符出现的次数作为权值。 3、生成huffman编码 从根节点出发,向左走为0,向右走为1,每次走一步,都将这个节点的huffman编码存储下来,直到走到叶子结点,huffman编码就能用递归直接获得。 4、将Huffman编码写入文件 使用哈希表,可以实现在O(1)时间内找到字符对应的编码,将每八位编码构成一个字符按“位”写入文件中。如果最后几位不够8位,在后面补0。最后解码时,根据字符个数,实现解压缩,总的字符个数就是Huffman数根节点的权值。 解压缩步骤1、读取解压文件读取文件中每个字符出现的次数,便于还原Huffman树。 2、构建huffman树构建Huffman树和压缩时的构建是一样的。 3、解压 首先在压缩文件中读取一个字符,然后解析这个字符的每一位,遇到一个叶子结点,就代表还原了一个字符,这时就将这个字符写到解压缩文件里。需要注意的是此时最后几位编码可能是自己补上去的,所以要用源文件中字符出现的次数来控制解压缩,根据Huffman性质可知,根节点权重就是字符出现的次数。 性能分析 文件名 文件大小 压缩大小 解压大小 压缩率 压缩时间 解压时间 haha.txt 7.12K 4.32K 7.12K 60.7% 11ms 5ms 人月神话.pdf 2.54M 2.11M 2.54M 83.7% 5658ms 2003ms 月弯弯.mp4 36.6M 36.5M 36.6M 99.8% 92480ms 31753ms 只要平凡.mp3 9.38M 9.37M 9.38M 99.9% 24427ms 9864ms 总结: 1、Huffman压缩适用于字符出现次数差值较大,分布不平均的文件。 2、对于文件二次压缩意义不大。 3、不能压缩文件夹。 4、从测试来看,能够处理大部分文件较好性能的压缩。 项目中遇到的问题1、解压缩时解压不完全 由于使用文本形式读取压缩文件,有可能提前遇到文件结束标志,所以要改为二进制形式读写。二进制形式是读取二进制编码。 如果以文本形式读取的话,回车会被当成一个字符’\n’,而二进制形式则会认为它是两个字符即’\r’回车、’\n’换行;如果在文本形式中遇到0x1B的话,文本形式会认为这是文本结束符,而二进制模型则不会对它产生处理。 2、得到huffman编码 在压缩时我们要求解huffman编码,在这里可以使用stl中的string和reverse求解。也可以使用后序递归直接求解。 3、压缩汉字时出现问题因为汉字是由多个字符表示,这些字符的范围是0—255,所以在结构体中要用unsigned char表示。 4、对文本文件最后一个字符的处理 补位数:压缩,解压无错误 改进方法1、解压缩的时候有可能要解压缩文件的不是用huffman进行压缩的文件。所以再解压文件之前先判断是不是用huffman进行压缩的。 2、不使用配置文件时如何解压 可以将huffman树的信息写入到压缩文件中。 ##项目扩展1、实现对文件夹的压缩对文件夹进行压缩实际上还是对文件夹中的内容进行压缩,所以得到一个文件夹之后我们就一直向子文件夹中找,直到找到文件后进行压缩就可以了。 2、解压缩的时候要将文件夹还原出来。 踩过的坑1、使用ifstrem和ofsteam函数对文本进行输出输入操作时,最好写成以二进制方式,否则可能会出现读取到特殊符号而终止,导致解压缩不完全,二进制方式如下: 12ifstream ifs(filename,ios::in|ios::binary);ofstream ofs(filename,ios::out|ios::binary); 2、对字符进行直接定址确定自己在哈希表中的位置时,要注意使用(unsigend char)ch强转,因为哈希表的定义范围是0到255,而字符的大小是-127到128 3、创建huffman树时,因为节点中保存的是一个结构体而不是一个简单的内置类型,因此在对节点进行“比较”操作的时候需要自己重载这些比较操作符,如: 12bool operator>(const T& t);bool operator!=(const T& t);]]></content>
<categories>
<category>小项目</category>
</categories>
<tags>
<tag>文件压缩</tag>
<tag>Huffman树</tag>
</tags>
</entry>
</search>