程序员笔试知识点整理
0、常考基础必知必会
A. 排序:排序有几种,各种排序的比较,哪些排序是稳定的,快排的算法;
B. 查找:哈希查找、二叉树查找、折半查找的对比,哈希映射和哈希表的区别?
C. 链表和数组的区别,在什么情况下用链表什么情况下用数组?
D. 栈和队列的区别?
E. 多态,举例说明;overload和override的区别?
F. 字符串有关的函数,比如让你写一个拷贝字符串的函数啊,或者字符串反转啊什么的。strcpy和memcpy?
G. 继承、多继承?
H. 面向对象有什么好处?
I. 说说static的与众不同之处,如果一个变量被声明为static,它会被分配在哪里?在什么时候分配空间等?
J. 什么是虚函数、纯虚函数、虚的析构函数,用途?
K. 内存泄漏及解决方法?
网络部分:
OSI模型7层结构,TCP/IP模型结构?
B. TCP/UDP区别?
C. TCP建立连接的步骤?
D. 香农定理?
1、二叉树三种遍历的非递归算法(背诵版)
本贴给出二叉树先序、中序、后序三种遍历的非递归算法,此三个算法可视为标准算法,直接用于考研答题。
1.先序遍历非递归算法
#define maxsize 100
typedef struct
{
Bitree Elem[maxsize];
int top;
}SqStack;
void PreOrderUnrec(Bitree t)
{
SqStack s;
StackInit(s);
p=t;
while (p!=null || !StackEmpty(s))
{
while (p!=null) //遍历左子树
{
visite(p->data);
push(s,p);
p=p->lchild;
}//endwhile
if (!StackEmpty(s)) //通过下一次循环中的内嵌while实现右子树遍历
{
p=pop(s);
p=p->rchild;
}//endif
}//endwhile
}//PreOrderUnrec
2.中序遍历非递归算法
#define maxsize 100
typedef struct
{
Bitree Elem[maxsize];
int top;
}SqStack;
void InOrderUnrec(Bitree t)
{
SqStack s;
StackInit(s);
p=t;
while (p!=null || !StackEmpty(s))
{
while (p!=null) //遍历左子树
{
push(s,p);
p=p->lchild;
}//endwhile
if (!StackEmpty(s))
{
p=pop(s);
visite(p->data); //访问根结点
p=p->rchild; //通过下一次循环实现右子树遍历
}//endif
}//endwhile
}//InOrderUnrec
3.后序遍历非递归算法
#define maxsize 100
typedef enum{L,R} tagtype;
typedef struct
{
Bitree ptr;
tagtype tag;
}stacknode;
typedef struct
{
stacknode Elem[maxsize];
int top;
}SqStack;
//后序遍历
void PostOrderUnrec(Bitree t)
{
SqStack s;
stacknode x;
StackInit(s);
p=t;
do
{
while (p!=null) //遍历左子树
{
x.ptr = p;
x.tag = L; //标记为左子树
push(s,x);
p=p->lchild;
}
while (!StackEmpty(s) &&s.Elem[s.top].tag==R)
{
x = pop(s);
p = x.ptr;
visite(p->data); //tag为R,表示右子树访问完毕,故访问根结点
}
if (!StackEmpty(s))
{
s.Elem[s.top].tag =R; //遍历右子树
p=s.Elem[s.top].ptr->rchild;
}
}while (!StackEmpty(s));
}//PostOrderUnrec
4.层次遍历算法
// 二叉树的数据结构
structBinaryTree
{
int value; // 不写模板了,暂时用整形代替节点的数据类型
BinaryTree *left;
BinaryTree *right;
};
BinaryTree*root; // 已知二叉树的根节点
//层次遍历
voidLevel( const BinaryTree *root )
{
Queue *buf = new Queue(); // 定义一个空队列,假设此队列的节点数据类型也是整形的
BinaryTree t; // 一个临时变量
buf.push_back(root); //令根节点入队
while( buf.empty == false ) // 当队列不为空
{
p = buf.front(); // 取出队列的第一个元素
cout<<p->value<<' ';
if( p->left != NULL ) // 若左子树不空,则令其入队
{
q.push( p->left );
}
if( p->right != NULL ) // 若右子树不空,则令其入队
{
q.push( p->right );
}
buf.pop(); // 遍历过的节点出队
}
cout<<endl;
}
2、线性表
(1) 性表的链式存储方式及以下几种常用链表的特点和运算:单链表、循环链表,双向链表,双向循环链表。
(2)单链表的归并算法、循环链表的归并算法、双向链表及双向循环链表的插入和删除算法等都是较为常见的考查方式。
(3)单链表中设置头指针、循环链表中设置尾指针而不设置头指针以及索引存储结构的各自好处。
3、栈与队列
你可以问一下自己是不是已经知道了以下几点:
(1)栈、队列的定义及其相关数据结构的概念,包括:顺序栈,链栈,共享栈,循环队列,链队等。栈与队列存取数据(请注意包括:存和取两部分)的特点。
(2)递归算法。栈与递归的关系,以及借助栈将递归转向于非递归的经典算法:n!阶乘问题,fib数列问题,hanoi问题,背包问题,二叉树的递归和非递归遍历问题,图的深度遍历与栈的关系等。其中,涉及到树与图的问题,多半会在树与图的相关章节中进行考查。
(3)栈的应用:数值表达式的求解,括号的配对等的原理,只作原理性了解,具体要求考查此为题目的算法设计题不多。
(4)循环队列中判队空、队满条件,循环队列中入队与出队(循环队列在插入时也要判断其是否已满,删除时要判断其是否已空)算法。
【循环队列的队空队满条件
为了方便起见,约定:初始化建空队时,令
front=rear=0,
当队空时:front=rear,
当队满时:front=rear 亦成立,
因此只凭等式front=rear无法判断队空还是队满。
有两种方法处理上述问题:
(1)另设一个标志位以区别队列是空还是满。
(2)少用一个元素空间,约定以“队列头指针front在队尾指针rear的下一个位置上”作为队列“满”状态的标志。
队空时: front=rear,
队满时: (rear+1)%maxsize=front】
如果你已经对上面的几点了如指掌,栈与队列一章可以不看书了。注意,我说的是可以不看书,并不是可以不作题哦。
循环队列的主要操作:
(1)创建循环队列
(2)初始化循环队列
(3)判断循环队列是否为空
(4)判断循环队列是否为满
(5)入队、出队
//空出头尾之间的一个元素不用
#include
#include
#define MAXSIZE 100
typedef struct
{
intelem[MAXSIZE];
intfront, rear;
}Quque; //定义队头
int initQue(Quque **q) //初始化
{
(*q)->front=0;
(*q)->rear=0;
}
int isFull(Quque *q)
{
if(q->front==(q->rear+1)%MAXSIZE)//判满(空出一个元素不用) 刘勉刚
return 1;
else
return 0;
}
int insertQue(Quque **q,int elem)
{
if(isFull(*q))return -1;
(*q)->elem[(*q)->rear]=elem;
(*q)->rear=((*q)->rear+1)%MAXSIZE;//插入
return0;
}
int isEmpty(Quque *q)
{
if(q->front==q->rear)//判空
return 1;
else
return 0;
}
int deleteQue(Quque ** q,int *pelem)
{
if(isEmpty(*q))
return 0;
*pelem=(*q)->elem[(*q)->front];
(*q)->front=((*q)->front +1)%MAXSIZE;
return0;
}
4、串
串一章需要攻破的主要堡垒有:
1. 串的基本概念,串与线性表的关系(串是其元素均为字符型数据的特殊线性表),空串与空格串的区别,串相等的条件;
2. 串的基本操作,以及这些基本函数的使用,包括:取子串,串连接,串替换,求串长等等。运用串的基本操作去完成特定的算法是很多学校在基本操作上的考查重点。
3. 顺序串与链串及块链串的区别和联系,实现方式。
4. KMP算法思想。KMP中next数组以及nextval数组的求法。明确传统模式匹配算法的不足,明确next数组需要改进。可能进行的考查方式是:求next和nextval数组值,根据求得的next或nextval数组值给出运用KMP算法进行匹配的匹配过程。
5、多维数组和广义表
矩阵包括:对称矩阵,三角矩阵,具有某种特点的稀疏矩阵等。
熟悉稀疏矩阵的三种不同存储方式:三元组,带辅助行向量的二元组,十字链表存储。
掌握将稀疏矩阵的三元组或二元组向十字链表进行转换的算法。
6、树与二叉树
树一章的知识点包括:
二叉树的概念、性质和存储结构,二叉树遍历的三种算法(递归与非递归),在三种基本遍历算法的基础上实现二叉树的其它算法,线索二叉树的概念和线索化算法以及线索化后的查找算法,最优二叉树的概念、构成和应用,树的概念和存储形式,树与森林的遍历算法及其与二叉树遍历算法的联系,树与森林和二叉树的转换。
(1) 二叉树的概念、性质和存储结构
考查方法可有:直接考查二叉树的定义,让你说明二叉树与普通双分支树(左右子树无序)的区别;考查满二叉树和完全二叉树的性质,普通二叉树的五个性质:
A.第i层的最多结点数,
B.深度为k的二叉树的最多结点数,
C.n0=n2+1的性质,
D.n个结点的完全二叉树的深度,
E. 顺序存储二叉树时孩子结点与父结点之间的换算关系(root从1开始,则左为:2*i,右为:2*i+1)。
二叉树的顺序存储和二叉链表存储的各自优缺点及适用场合,二叉树的三叉链表表示方法。
(2) 二叉树的三种遍历算法
这一知识点掌握的好坏,将直接关系到树一章的算法能否理解,进而关系到树一章的算法设计题能否顺利完成。二叉树的遍历算法有三种:先序,中序和后序。其划分的依据是视其每个算法中对根结点数据的访问顺序而定。不仅要熟练掌握三种遍历的递归算法,理解其执行的实际步骤,并且应该熟练掌握三种遍历的非递归算法。由于二叉树一章的很多算法,可以直接根据三种递归算法改造而来(比如:求叶子个数),所以,掌握了三种遍历的非递归算法后,对付诸如:“利用非递归算法求二叉树叶子个数”这样的题目就下笔如有神了。
(3) 可在三种遍历算法的基础上改造完成的其它二叉树算法:
求叶子个数,求二叉树结点总数,求度为1或度为2的结点总数,复制二叉树,建立二叉树,交换左右子树,查找值为n的某个指定结点,删除值为n的某个指定结点,诸如此类等等等等。如果你可以熟练掌握二叉树的递归和非递归遍历算法,那么解决以上问题就是小菜一碟了。
(4) 线索二叉树:
线索二叉树的引出,是为避免如二叉树遍历时的递归求解。众所周知,递归虽然形式上比较好理解,但是消耗了大量的内存资源,如果递归层次一多,势必带来资源耗尽的危险,为了避免此类情况,线索二叉树便堂而皇之地出现了。对于线索二叉树,应该掌握:线索化的实质,三种线索化的算法,线索化后二叉树的遍历算法,基本线索二叉树的其它算法问题(如:查找某一类线索二叉树中指定结点的前驱或后继结点就是一类常考题)。
(5) 最优二叉树(哈夫曼树):
最优二叉树是为了解决特定问题引出的特殊二叉树结构,它的前提是给二叉树的每条边赋予了权值,这样形成的二叉树按权相加之和是最小的。最优二叉树一节,直接考查算法源码的很少,一般是给你一组数据,要求你建立基于这组数据的最优二叉树,并求出其最小权值之和,此类题目不难,属送分题。
(6) 树与森林:
二叉树是一种特殊的树,这种特殊不仅仅在于其分支最多为2以及其它特征,一个最重要的特殊之处是在于:二叉树是有序的!即:二叉树的左右孩子是不可交换的,如果交换了就成了另外一棵二叉树。 树与森林的遍历,不像二叉树那样丰富,他们只有两种遍历算法:先根与后根(对于森林而言称作:先序与后序遍历)。此二者的先根与后根遍历与二叉树中的遍历算法是有对应关系的:先根遍历对应二叉树的先序遍历,而后根遍历对应二叉树的中序遍历。二叉树、树与森林之所以能有以上的对应关系,全拜二叉链表所赐。二叉树使用二叉链表分别存放他的左右孩子,树利用二叉链表存储孩子及兄弟(称孩子兄弟链表),而森林也是利用二叉链表存储孩子及兄弟。
7、图
1. 图的基本概念:图的定义和特点,无向图,有向图,入度,出度,完全图,生成子图,路径长度,回路,(强)连通图,(强)连通分量等概念。
2. 图的几种存储形式:邻接矩阵,(逆)邻接表,十字链表及邻接多重表。在考查时,有的学校是给出一种存储形式,要求考生用算法或手写出与给定的结构相对应的该图的另一种存储形式。
3. 考查图的两种遍历算法:深度遍历和广度遍历
深度遍历和广度遍历是图的两种基本的遍历算法,这两个算法对图一章的重要性等同于“先序、中序、后序遍历”对于二叉树一章的重要性。在考查时,图一章的算法设计题常常是基于这两种基本的遍历算法而设计的,比如:“求最长的最短路径问题”和“判断两顶点间是否存在长为K的简单路径问题”,就分别用到了广度遍历和深度遍历算法。
4. 生成树、最小生成树的概念以及最小生成树的构造:PRIM算法和KRUSKAL算法。
考查时,一般不要求写出算法源码,而是要求根据这两种最小生成树的算法思想写出其构造过程及最终生成的最小生成树。
5. 拓扑排序问题:
拓扑排序有两种方法,一是无前趋的顶点优先算法,二是无后继的顶点优先算法。换句话说,一种是“从前向后”的排序,一种是“从后向前”排。当然,后一种排序出来的结果是“逆拓扑有序”的。
6. 关键路径问题:
这个问题是图一章的难点问题。理解关键路径的关键有三个方面:
一是何谓关键路径;
二是最早时间是什么意思、如何求;
三是最晚时间是什么意思、如何求。
简单地说,最早时间是通过“从前向后”的方法求的,而最晚时间是通过“从后向前”的方法求解的,并且,要想求最晚时间必须是在所有的最早时间都已经求出来之后才能进行。
在实际设计关键路径的算法时,还应该注意以下这一点:采用邻接表的存储结构,求最早时间和最晚时间要采用不同的处理方法,即:在算法初始时,应该首先将所有顶点的最早时间全部置为0。关键路径问题是工程进度控制的重要方法,具有很强的实用性。
7. 最短路径问题:
与关键路径问题并称为图一章的两只拦路虎。概念理解是比较容易的,关键是算法的理解。最短路径问题分为两种:一是求从某一点出发到其余各点的最短路径(单源最短路径);二是求图中每一对顶点之间的最短路径。这个问题也具有非常实用的背景特色,一个典型的应该就是旅游景点及旅游路线的选择问题。解决第一个问题用DIJSKTRA算法,解决第二个问题用FLOYD算法,注意区分。
8、查找(search)
先弄清楚以下几个概念:关键字、主关键字、次关键字的含义;静态查找与动态查找的含义及区别;平均查找长度ASL的概念及在各种查找算法中的计算方法和计算结果,特别是一些典型结构的ASL值,应该记住。
一般将search分为三类:在顺序表上的查找;在树表上的查找;在哈希表上的查找。
(1) 线性表上的查找:
主要分为三种线性结构:
顺序表——传统查找方法:逐个比较;
有序顺序表——二分查找法(注意适用条件以及其递归实现方法);
索引顺序表——对索引结构,采用索引查找算法。注意这三种表下的ASL值以及三种算法的实现。
(2) 树表上的查找:
树表主要分为以下几种:二叉排序树(即二叉查找树),平衡二叉查找树(AVL树),B树,键树。其中,尤以前两种结构为重,也有部分名校偏爱考B树的。由于二叉排序树与平衡二叉树是一种特殊的二叉树。
二叉排序树,简言之,就是“左小右大”,它的中序遍历结果是一个递增的有序序列。平衡二叉排序树是二叉排序树的优化,其本质也是一种二叉排序树,只不过,平衡排序二叉树对左右子树的深度有了限定:深度之差的绝对值不得大于1。对于二叉排序树,“判断某棵二叉树是否二叉排序树”这一算法经常被考到,可用递归,也可以用非递归。平衡二叉树的建立也是一个常考点,但该知识点归根结底还是关注的平衡二叉树的四种调整算法,调整的一个参照是:调整前后的中序遍历结果相同。
B树是二叉排序树的进一步改进,也可以把B树理解为三叉、四叉....排序树。除B树的查找算法外,应该特别注意一下B树的插入和删除算法,因为这两种算法涉及到B树结点的分裂和合并,是一个难点。 键树(keywordtree),又称数字搜索树(digitalsearch tree)或字符树。trie树也可说等同于键树或属于键树的一种。键树特别适用于查找英文单词的场合。一般不要求能完整描述算法源码,多是根据算法思想建立键树及描述其大致查找过程。
(3) 基于哈希表的查找算法:
哈希译自“hash”一词,意为“散列”或“杂凑”。哈希表查找的基本思想是:根据当前待查找数据的特征,以记录关键字为自变量,设计一个function,该函数对关键字进行转换后,其解释结果为待查的地址。基于哈希表的考查点有:哈希函数的设计,冲突解决方法的选择及冲突处理过程的描述。
9、内部排序
考查你对书本上的各种排序算法及其思想以及其优缺点和性能指标(时间复杂度)能否了如指掌。
排序方法分类有:插入、选择、交换、归并、计数等五种排序方法。
(1)插入排序中又可分为:直接插入、折半插入、2路插入(?)、希尔排序。这几种插入排序算法的最根本的不同点,说到底就是根据什么规则寻找新元素的插入点。直接插入是依次寻找,折半插入是折半寻找,希尔排序,是通过控制每次参与排序的数的总范围“由小到大”的增量来实现排序效率提高的目的。
(2)交换排序,又称冒泡排序,在交换排序的基础上改进又可以得到快速排序。快速排序的思想,一语以敝之:用中间数将待排数据组一分为二。
(3)选择排序可以分为:简单选择、树选择、堆排序。选择排序相对于前面几种排序算法来说,难度大一点。这三种方法的不同点是,根据什么规则选取最小的数。
简单选择,是通过简单的数组遍历方案确定最小数;
树选择,是通过“锦标赛”类似的思想,让两数相比,不断淘汰较大(小)者,最终选出最小(大)数;
而堆排序,是利用堆这种数据结构的性质,通过堆元素的删除、调整等一系列操作将最小数选出放在堆顶。堆排序中的堆建立、堆调整是重要考点。
(4)归并排序,是通过“归并”这种操作完成排序的目的,既然是归并就必须是两者以上的数据集合才可能实现归并。所以,在归并排序中,关注最多的就是2路归并。算法思想比较简单,有一点,要铭记在心:归并排序是稳定排序。
(5)基数排序,是一种很特别的排序方法,也正是由于它的特殊,所以,基数排序就比较适合于一些特别的场合,比如扑克牌排序问题等。基数排序,又分为两种:多关键字的排序(扑克牌排序),链式排序(整数排序)。基数排序的核心思想也是利用“基数空间”这个概念将问题规模规范、变小,并且,在排序的过程中,只要按照基排的思想,是不用进行关键字比较的,这样得出的最终序列就是一个有序序列。
本章各种排序算法的思想以及伪代码实现,及其时间复杂度都是必须掌握的。
//稳定性分析
排序算法的稳定性,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。
稳定性的好处:若排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,对基于比较的排序算法而言,元素交换的次数可能会少一些(个人感觉,没有证实)。
分析一下常见的排序算法的稳定性,每个都给出简单的理由。
(1) 冒泡排序
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
(2) 选择排序
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
(3) 插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
(4) 快速排序
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j]> a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和 a[j] 交换的时刻。
(5) 归并排序
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
(6) 基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
(7) 希尔排序(shell)
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
(8) 堆排序
我们知道堆的结构是节点i的孩子为2*i和2*i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n /2-1, n/2-2, ...1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
//
冒泡排序 插入排序 二路插入排序 希尔排序 快速排序 选择排序 归并排序 堆排序算法的C/C++实现//
#include <iostream>
using namespace std;
//交换两个数的值
void swap(int &a,int &b)
{
int tmp;
tmp=a;
a=b;
b=tmp;
}
//屏幕输出数组
void display(int array[],int len)
{
cout<<"the resultis:"<<endl;
for (int i = 0 ;i < len;i++ )
{
cout<<array[i]<<" ";
}
cout<<endl;
}
/*
冒泡排序
算法思想:将被排序的记录数组R[1..n]垂直排列,每个记录R[i]看作是重量为R[i].key的气泡。
根据轻气泡不能在重气泡之下的原则,从下往上扫描数组 R:凡扫描到违反本原则的
轻气泡,就使其向上"飘浮"。如此反复进行,直到最后任何两个气泡都是轻者在上,
重者在下为止。
时间复杂度 o(n^2)
空间复杂度 o(1)
比较次数 n(n+1)/2
*/
void bubble_sort(int array[],int len)
{
for (int i = len-1 ;i >= 0;i-- )
{
for(int j = 0;j < i;j++)
if(array[j] > array[j+1])
swap(array[j],array[j+1]);
}
}
/*
直接插入排序
算法思想:把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元
素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,将它
插入到有序表中的适当位置,使之成为新的有序表,重复n-1次可完成排序过程。
时间复杂度 o(n^2)
空间复杂度 o(1)
比较次数 n(n+1)/2
*/
void insert_sort(int array[],int len)
{
int tmp,i,j;
for(i = 1;i < len;i++)
{
if (array[i] < array[i-1])
{
tmp = array[i];
array[i] = array[i-1];
//插入到相应位置
for (j = i-2;j >= 0;j--)
{
//往后移
if (array[j] > tmp)
array[j+1] =array[j];
else
{
array[j+1] = tmp;
break;
}
}
if(j == -1)
array[j+1] = tmp;
}
}
}
/*
2-路插入排序
算法思想:增加一个辅助空间d,把r[1]赋值给d[1],并将d[1]看成是排好序后处于中间
位置的记录。然后从r[2]开始依次插入到d[1]之前或之后的有序序列中。
时间复杂度 o(n^2)
空间复杂度 o(1)
比较次数 n(n+1)/2
*/
void bi_insert_sort(int array[],int len)
{
int* arr_d = (int*)malloc(sizeof(int) * len);
arr_d[0] = array[0];
int head = 0,tail = 0;
for (int i = 1;i < len; i++ )
{
if (array[i] > arr_d[0])
{
int j;
for ( j= tail;j>0;j--)
{
if (array[i] <arr_d[j])
arr_d[j+1] =arr_d[j];
else
break;
}
arr_d[j+1] = array[i];
tail += 1;
}
else
{
if (head ==0)
{
arr_d[len-1] = array[i];
head =len-1;
}
else
{
int j;
for (j = head;j <=len-1;j++)
{
if (array[i] >arr_d[j])
arr_d[j-1] =arr_d[j];
else
break;
}
arr_d[j-1] = array[i];
head -= 1;
}
}
}
for (int i = 0;i < len; i++)
{
int pos = (i + head );
if(pos >= len) pos -= len;
array[i] = arr_d[pos];
}
free(arr_d);
}
/*
希尔排序
算法思想:先将整个待排序记录分割成若干子序列分别进行直接插入排
序,待整个序列中的记录基本有序时,再对全体记录进行一
次直接插入排序
时间复杂度 o(n^2)
空间复杂度 o(1)
比较次数 ?
*/
void shell_insert(int array[],int d,int len)
{
int tmp,j;
for (int i = d;i < len;i++)
{
if(array[i] < array[i-d])
{
tmp = array[i];
j = i - d;
do
{
array[j+d] = array[j];
j = j - d;
} while (j >= 0 &&tmp < array[j]);
array[j+d] = tmp;
}
}
}
void shell_sort(int array[],int len)
{
int inc = len;
do
{
inc = inc/2;
shell_insert(array,inc,len);
} while (inc > 1);
}
/*
快速排序
算法思想:将原问题分解为若干个规模更小但结构与原问题相似的子问题。
递归地解这些子问题,然后将这些子问题的解组合成为原问题的解。
时间复杂度 o(nlogn)
空间复杂度 o(logn)
比较次数 ?
*/
void quick_sort(int array[],int low,int high)
{
if (low < high)
{
int pivotloc =partition(array,low,high);
quick_sort(array,low,pivotloc-1);
quick_sort(array,pivotloc+1,high);
}
}
int partition(int array[],int low,int high)
{
int pivotkey = array[low];
while (low < high)
{
while(low < high &&array[high] >= pivotkey)
--high;
swap(array[low],array[high]);
while(low < high &&array[low] <= pivotkey)
++low;
swap(array[low],array[high]);
}
array[low] = pivotkey;
return low;
}
/*
直接选择排序
算法思想:每一趟在n-i+1个记录中选取关键字最小的记录作为有序序列中的第i个记录
时间复杂度 o(n^2)
空间复杂度 o(1) ?
比较次数 n(n+1)/2
*/
int SelectMinKey(int array[],int iPos,int len)
{
int ret = 0;
for (int i = iPos; i < len; i++)
{
if (array[ret] > array[i])
{
ret = i;
}
}
return ret;
}
void select_sort(int array[],int len)
{
for (int i = 0; i < len; i++)
{
int j =SelectMinKey(array,i,len);
if (i != j)
{
swap(array[i],array[j]);
}
}
}
/*
归并排序
算法思想:设两个有序的子文件(相当于输入堆)放在同一向量中相邻的位置上:R[low..m],R[m+1..high],先将它们合并到一个局部的暂存向量R1(相当于输出堆)中,待合并完成后将R1复制回R[low..high]中。
时间复杂度 o(nlogn)
空间复杂度 o(n)
比较次数 ?
*/
void merge(int array[],int i,int m, int n)
{
int j, k;
int iStart = i, iEnd = n;
int arrayDest[256];
for ( j = m + 1,k = i; i <= m&& j <= n; ++k)
{
if (array[i] < array[j])
arrayDest[k] = array[i++];
else
arrayDest[k] = array[j++];
}
if (i <= m)
for (;k <= n; k++,i++)
arrayDest[k] = array[i];
if(j <= n)
for (;k <= n; k++,j++)
arrayDest[k] = array[j];
for(j = iStart; j <= iEnd; j++)
array[j] = arrayDest[j];
}
void merge_sort(int array[],int s,int t)
{
int m;
if (s < t)
{
m = (s + t )/2;
merge_sort(array,s,m);
merge_sort(array,m+1,t);
merge(array,s,m,t);
}
}
/*
堆排序
算法思想:堆排序(Heap Sort)是指利用堆(heaps)这种数据结构来构造的一种排序算法。
堆是一个近似完全二叉树结构,并同时满足堆属性:即子节点的键值或索引总是
小于(或者大于)它的父节点。
时间复杂度 o(nlogn)
空间复杂度 o(1)
比较次数:较多
*/
void heap_adjust(int array[],int i,int len)
{
int rc = array[i];
for(int j = 2 * i; j <len; j *= 2)
{
if(j < len && array[j]< array[j+1]) j++;
if(rc >= array[j]) break;
array[i] = array[j]; i = j;
}
array[i] = rc;
}
void heap_sort(int array[],int len)
{
int i;
for(i = (len-1)/2; i >= 0; i--)
heap_adjust(array,i,len);
for( i = (len-1); i > 0; i--)
{
swap(array[0],array[i]); //弹出最大值,重新对i-1个元素建堆
heap_adjust(array,0,i-1);
}
}
int main()
{
int array[] = {45, 56, 76, 234, 1, 34,23, 2, 3, 55, 88, 100};
int len = sizeof(array)/sizeof(int);
//bubble_sort(array,len); //冒泡排序
/*insert_sort(array,len);*/ //插入排序
/*bi_insert_sort(array,len);*/ //二路插入排序
/*shell_sort(array,len);*/ //希尔排序
/*quick_sort(array,0,len-1);*/ //快速排序
/*select_sort(array,len);*/ //选择排序
/*merge_sort(array,0,len-1);*/ //归并排序
heap_sort(array,len); //堆排序
display(array,len);
return 0;
}
<|>对排序算法的总结
按平均时间将排序分为四类:
(1)平方阶(O(n2))排序
一般称为简单排序,例如直接插入、直接选择和冒泡排序;
(2)线性对数阶(O(nlgn))排序
如快速、堆和归并排序;
(3)O(n1+£)阶排序
£是介于0和1之间的常数,即0<£<1,如希尔排序;
(4)线性阶(O(n))排序
如桶、箱和基数排序。
各种排序方法比较
简单排序中直接插入最好,快速排序最快,当文件为正序时,直接插入和冒泡均最佳。
影响排序效果的因素
因为不同的排序方法适应不同的应用环境和要求,所以选择合适的排序方法应综合考虑下列因素:
①待排序的记录数目n;
②记录的大小(规模);
③关键字的结构及其初始状态;
④对稳定性的要求;
⑤语言工具的条件;
⑥存储结构;
⑦时间和辅助空间复杂度等。
不同条件下,排序方法的选择
(1)若n较小(如n≤50),可采用直接插入或直接选择排序。
当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
(2)若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜;
(3)若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
若要求排序稳定,则可选用归并排序。但本章介绍的从单个记录起进行两两归并的 排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。
10、OSI模型7层结构,TCP/IP模型结构?
osi参考模型
osi参考模型中的数据封装过程
下面的图表试图显示不同的TCP/IP和其他的协议在最初OSI模型中的位置:
7 | 应用层 | 例如HTTP、SMTP、SNMP、FTP、Telnet、SIP、SSH、NFS、RTSP、XMPP、Whois、ENRP |
6 | 表示层 | |
5 | 会话层 | 例如ASAP、TLS、SSH、ISO 8327 / CCITT X.225、RPC、NetBIOS、ASP、Winsock、BSD sockets |
4 | 传输层 | |
3 | 网络层 | |
2 | 数据链路层 | 例如Ethernet、Token ring、HDLC、Frame relay、ISDN、ATM、802.11 WiFi、FDDI、PPP |
1 | 物理层 |
tcp/ip参考模型
tcp/ip参考模型分为四个层次:应用层、传输层、网络互连层和主机到网络层:
tcp/ip参考模型的层次结构
通常人们认为OSI模型的最上面三层(应用层、表示层和会话层)在TCP/IP组中是一个应用层。由于TCP/IP有一个相对较弱的会话层,由TCP和RTP下的打开和关闭连接组成,并且在TCP和UDP下的各种应用提供不同的端口号,这些功能能够被单个的应用程序(或者那些应用程序所使用的库)增加。与此相似的是,IP是按照将它下面的网络当作一个黑盒子的思想设计的,这样在讨论TCP/IP的时候就可以把它当作一个独立的层。
4 | 应用层 | 例如HTTP、FTP、DNS |
3 | 传输层 | |
2 | 网络互连层 | 对于TCP/IP来说这是因特网协议(IP) |
1 | 网络接口层 |
应用层
该层包括所有和应用程序协同工作,利用基础网络交换应用程序专用的数据的协议。应用层是大多数普通与网络相关的程序为了通过网络与其他程序通信所使用的层。这个层的处理过程是应用特有的;数据从网络相关的程序以这种应用内部使用的格式进行传送,然后被编码成标准协议的格式。
一些特定的程序被认为运行在这个层上。它们提供服务直接支持用户应用。这些程序和它们对应的协议包括HTTP(The WorldWide Web)、FTP(文件传输)、SMTP(电子邮件)、SSH(安全远程登陆)、DNS(名称<-> IP 地址寻找)以及许多其他协议。
一旦从应用程序来的数据被编码成一个标准的应用层协议,它将被传送到IP栈的下一层。
在传输层,应用程序最常用的是TCP或者UDP,并且服务器应用程序经常与一个公开的端口号相联系。服务器应用程序的端口由InternetAssigned Numbers Authority(IANA)正式地分配,但是现今一些新协议的开发者经常选择它们自己的端口号。由于在同一个系统上很少超过少数几个的服务器应用,端口冲突引起的问题很少。应用软件通常也允许用户强制性地指定端口号作为运行参数。
连结外部的客户端程序通常使用系统分配的一个随机端口号。监听一个端口并且然后通过服务器将那个端口发送到应用的另外一个副本以建立对等连结(如IRC上的dcc文件传输)的应用也可以使用一个随机端口,但是应用程序通常允许定义一个特定的端口范围的规范以允许端口能够通过实现网络地址转换(NAT)的路由器映射到内部。
每一个应用层(TCP/IP参考模型 的最高层)协议一般都会使用到两个传输层协议之一:面向连接的TCP传输控制协议和无连接的包传输的UDP用户数据报文协议 。
常用的应用层协议有:
运行在TCP协议上的协议:
- HTTP(HypertextTransfer Protocol,超文本传输协议),主要用于普通浏览。
- HTTPS(HypertextTransfer Protocol over Secure Socket Layer, or HTTP over SSL,安全超文本传输协议),HTTP协议的安全版本。
- FTP(File Transfer Protocol,文件传输协议),由名知义,用于文件传输。
- POP3(PostOffice Protocol, version 3,邮局协议),收邮件用。
- SMTP(SimpleMail Transfer Protocol,简单邮件传输协议),用来发送电子邮件 。
- TELNET(Teletypeover the Network,网络电传),通过一个终端(terminal)登陆到网络。
- SSH(Secure Shell,用于替代安全性差的TELNET),用于加密安全登陆。
运行在UDP协议上的协议:
其他:
- DNS(Domain Name Service,域名服务),用于完成地址查找,邮件转发等工作(运行在TCP和UDP协议上)。
- ECHO(EchoProtocol,回绕协议),用于查错及测量应答时间(运行在TCP和UDP协议上)。
- SNMP(SimpleNetwork Management Protocol,简单网络管理协议),用于网络信息的收集和网络管理。
- DHCP(DynamicHost Configuration Protocol,动态主机配置协议),动态配置IP地址。
- ARP(Address Resolution Protocol,地址解析协议),用于动态解析以太网硬件的地址。
传输层
传输层的协议,能够解决诸如可靠性(“数据是否已经到达目的地?”)和保证数据按照正确的顺序到达这样的问题。在TCP/IP协议组中,传输协议也包括所给数据应该送给哪个应用程序。
在TCP/IP协议组中技术上位于这个层的动态路由协议通常被认为是网络层的一部分;一个例子就是OSPF(IP协议89)。
TCP(IP协议6)是一个“可靠的”、面向连结的传输机制,它提供一种可靠的字节流保证数据完整、无损并且按顺序到达。TCP尽量连续不断地测试网络的负载并且控制发送数据的速度以避免网络过载。另外,TCP试图将数据按照规定的顺序发送。这是它与UDP不同之处,这在实时数据流或者路由高网络层丢失率应用的时候可能成为一个缺陷。
较新的SCTP也是一个“可靠的”、面向连结的传输机制。它是面向纪录而不是面向字节的,它在一个单独的连结上提供了通过多路复用提供的多个子流。它也提供了多路自寻址支持,其中连结终端能够被多个IP地址表示(代表多个物理接口),这样的话即使其中一个连接失败了也不中断。它最初是为电话应用开发的(在IP上传输SS7),但是也可以用于其他的应用。
UDP(IP协议号17)是一个无连结的数据报协议。它是一个“best effort”或者“不可靠”协议——不是因为它特别不可靠,而是因为它不检查数据包是否已经到达目的地,并且不保证它们按顺序到达。如果一个应用程序需要这些特点,它必须自己提供或者使用TCP。
UDP的典型性应用是如流媒体(音频和视频等)这样按时到达比可靠性更重要的应用,或者如DNS查找这样的简单查询/响应应用,如果建立可靠的连结所作的额外工作将是不成比例地大。
DCCP目前正由IEFT开发。它提供TCP流动控制语义,但对于用户来说保留了UDP的数据报服务模型。
TCP和UDP都用来支持一些高层的应用。任何给定网络地址的应用通过它们的TCP或者UDP端口号区分。根据惯例使一些大众所知的端口与特定的应用相联系。
RTP是为如音频和视频流这样的实时数据设计的数据报协议。RTP是使用UDP包格式作为基础的会话层,然而据说它位于因特网协议栈的传输层。
网络互连层
正如最初所定义的,网络层解决在一个单一网络上传输数据包的问题。类似的协议有X.25和ARPANET的Host/IMP Protocol。
随着因特网思想的出现,在这个层上添加了附加的功能,也就是将数据从源网络传输到目的网络。这就牵涉到在网络组成的网上选择路径将数据包传输,也就是因特网。
在因特网协议组中,IP完成数据从源发送到目的基本任务。IP能够承载多种不同的高层协议的数据;这些协议使用一个唯一的IP协议号进行标识。ICMP和IGMP分别是1和2。
一些IP承载的协议,如ICMP(用来发送关于IP发送的诊断信息)和IGMP(用来管理多播数据),它们位于IP层之上但是完成网络层的功能,这表明了因特网和OSI模型之间的不兼容性。所有的路由协议,如BGP、 OSPF、和RIP实际上也是网络层的一部分,尽管似乎它们应该属于更高的协议栈。
网络接口层
网络接口层实际上并不是因特网协议组中的一部分,但是它是数据包从一个设备的网络层传输到另外一个设备的网络层的方法。这个过程能够在网卡的软件驱动程序中控制,也可以在韧体或者专用芯片中控制。这将完成如添加报头准备发送、通过物理媒介实际发送这样一些数据链路功能。另一端,链路层将完成数据帧接收、去除报头并且将接收到的包传到网络层。
然而,链路层并不经常这样简单。它也可能是一个虚拟专有网络(VPN)或者隧道,在这里从网络层来的包使用隧道协议和其他(或者同样的)协议组发送而不是发送到物理的接口上。VPN和隧道通常预先建好,并且它们有一些直接发送到物理接口所没有的特殊特点(例如,它可以加密经过它的数据)。由于现在链路“层”是一个完整的网络,这种协议组的递归使用可能引起混淆。但是它是一个实现常见复杂功能的一个优秀方法。(尽管需要注意预防一个已经封装并且经隧道发送下去的数据包进行再次地封装和发送)。
///
1、物理层(physical layer)
物理层规定了激活、维持、关闭通信端点之间的机械特性、电气特性、功能特性以及过程特性。该层为上层协议提供了一个传输数据的物理媒体。
在这一层,数据的单位称为比特(bit)。
属于物理层定义的典型规范代表包括:eia/tia rs-232、eia/tia rs-449、v.35、rj-45等。
2、数据链路层(data link layer)
数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。
在这一层,数据的单位称为帧(frame)。
数据链路层协议的代表包括:sdlc、hdlc、ppp、stp、帧中继等。
3、网络层(network layer)
网络层负责对子网间的数据包进行路由选择。此外,网络层还可以实现拥塞控制、网际互连等功能。
在这一层,数据的单位称为数据包(packet)。
网络层协议的代表包括:ip、ipx、rip、ospf等。
4、传输层(transport layer)
传输层是第一个端到端,即主机到主机的层次。传输层负责将上层数据分段并提供端到端的、可靠的或不可靠的传输。此外,传输层还要处理端到端的差错控制和流量控制问题。 在这一层,数据的单位称为数据段(segment)。
传输层协议的代表包括:tcp、udp、spx等。
5、会话层(session layer)
会话层管理主机之间的会话进程,即负责建立、管理、终止进程之间的会话。会话层还利用在数据中插入校验点来实现数据的同步。
会话层协议的代表包括:netbios、zip(appletalk区域信息协议)等。
6、表示层(presentation layer)
表示层对上层数据或信息进行变换以保证一个主机应用层信息可以被另一个主机的应用程序理解。表示层的数据转换包括数据的加密、压缩、格式转换等。
表示层协议的代表包括:ascii、asn.1、jpeg、mpeg等。
7、应用层(application layer)
应用层为操作系统或网络应用程序提供访问网络服务的接口。
应用层协议的代表包括:telnet、ftp、http、snmp等。
集线器hub工作在OSI参考模型的(物理)层;
网卡工作在OSI参考模型的(物理)层;
路由器router工作在OSI参考模型的(网络)层;
交换机Switch工作在OSI参考模型的(数据链路)层。
///
(附)10、tcp建立连接为什么要三次握手?
tcp是一个面向连接的协议,在传送数据以前,必须要首先建立一条连接。连接的建立需要经过三次握手。为什么要经过三次握手呢,每次握手双方都做了些什么?
1)什么是tcp报文?
tcp报文就是通过tcp协议发送的数据包,由tcp头和数据段组成。
tcp头是固定的20个字节,它的格式为:
2)第一次握手做什么?
请求端(客户端)会向服务端(被请求端)发送一个tcp报文,申请打开某一个端口。因为没有数据,所以这个报文仅包含一个tcp头。其中:
SYN=1;当建立一个新的连接时, SYN标志变1。
序号;序号用来标识从客户端向服务端发送的数据字节流。
此时客户端进入SYN_SENT状态。
3)第二次握手做什么?
服务端收到客户端的SYN包,也会发一个只包含tcp头的报文给客户端。
ACK=1;服务端确认收到信息
确认序号;客户端序号+1,作为应答
SYN=1;因为tcp的连接是双向的,服务端作为应答的同时请求建立连接。
此时服务端进入SYN_RECV状态
4)第三次握手做什么?
ACK=1;客户端确认收到信息
确认序号;服务端序号+1,作为应答
此时客户端进入ESTABLISHED状态,服务端收到ACK后也会进入此状态
可见,客户端和服务端都保留了对方的序号,这三次握手缺少任何一步都无法实现这一目标。在三次握手过程中,出现了一些中间状态。
5)什么是半连接队列?
第一次握手完成后,服务端发送ACK+SYN包到客户端,在收到客户端返回前的状态为SYN_RECV,服务端为此状态维护一个半连接队列。当服务端收到客户的确认包时,删除该条目,服务端进入ESTABLISHED状态。Listen中的backlog参数表示这两个状态合的最大值。若客户端完成第一次握手后不再发送ACK包,导致服务端未完成队列溢出,达到Dos攻击的目的。
6)什么是SYN-ACK 重传?
Dos攻击可以达到目的的一个重要因素是服务端在发送完SYN+ACK包后会等待客户端的确认包,如果等待时间内未收到,服务端会进行首次重传,等待一段时间仍未收到客户确认包,会进行第二次重传,直到重传次数超过系统规定的最大值,系统将该连接信息从半连接队列中删除。如果系统删除的频率小于半连接状态的增长频率,服务端就无法正常提供服务。
7)Tcp关闭连接需要四次握手,这又是为什么呢?
这是由tcp半关闭(harf-close)造成的。既然一个TCP连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。即一方发送一个FIN,另一方收到后发送一个ACK,这就是所谓的四次握手了。
8)第一次握手做什么?
客户端发送一个FIN(这个客户端是主动发起关闭的一端,与建立连接时的客户端不一定是同一主机)
此时客户端进入FIN_WAIT_1状态。
9)第二次握手做什么?
服务端收到FIN,发回客户端一个ACK,确认序号为收到的序号加1(因为FIN和SYN一样,会占用一个序号);客户端收到ACK之后会进入FIN_WAIT_2状态,服务端会进入CLOSE_WAIT状态。
10)第三次握手做什么?
服务端发送给客户端一个FIN。服务端进入LAST_ACK状态。
11)第四次握手做什么?
客户端收到FIN,发回服务端一个ACK,确认序号为收到的序号加1;客户端会进入TIME_WAIT状态,2MSL超时后进入CLOSE状态。服务端收到ACK后也会进入CLOSE状态。
其实我们通俗的说每次握手其实就是发一次数据包的过程。建立连接时双方共发送了3个包,关闭连接时发送和确认的两次握手决定了一端数据流的关闭,四次握手可以保证两方都关闭。
12)为什么建立连接是三次握手,而关闭连接是四次呢?
建立连接时,服务端可以把应答ACK和同步SYN放在一个报文里进行发送。而关闭连接时,收到FIN通知仅仅表示对方没有数据发送过来了,并不表示自己的数据全部发送给了对方。所以ACK和FIN是分了两次进行发送。如果服务端收到FIN,恰恰自己也没有数据要发,是不是ACK和FIN可以一起发给客户端呢,这样就可以少一次数据流了。世界是美好的,经典的TCP连接状态图中也考虑到了这种情况,tcp关闭连接确实是只有三次数据流动,服务端将ACK和FIN放在一个包里进行发送,但四次握手这个概念却已经根深蒂固无法更改了。
13)Tcp的各个状态是怎样的?
客户端的正常tcp状态:
CLOSED->SYN_SENT(第1次)->ESTABLISHED(第3次)->FIN_WAIT_1(第1次)->FIN_WAIT_2(第2次)->TIME_WAIT(第4次)->CLOSED
服务端的正常tcp状态:
CLOSED->LISTEN->SYN_RCVD(第2次)->ESTABLISHED(第3次)->CLOSE_WAIT(第2次)->LAST_ACK(第3次)->CLOSED(第4次)
tcp还有其他的非正常状态,在此不做讨论,下篇文章再说。
11、数组和链表的优缺点
数组,在内存上给出了连续的空间。链表,内存地址上可以是不连续的,每个链表的节点包括原来的内存和下一个节点的信息(单向的一个,双向链表的话,会有两个)。
数组优于链表的:
A. 内存空间占用的少,因为链表节点会附加上一块或两块下一个节点的信息。
但是数组在建立时就固定了。所以也有可能会因为建立的数组过大或不足引起内存上的问题。
B. 数组内的数据可随机访问,但链表不具备随机访问性。这个很容易理解,数组在内存里是连续的空间,比如如果一个数组地址从100到200,且每个元素占用两个字节,那么100-200之间的任何一个偶数都是数组元素的地址,可以直接访问。
链表在内存地址可能是分散的。所以必须通过上一节点中的信息找能找到下一个节点。
C. 查找速度上。这个也是因为内存地址的连续性的问题,不罗索了。
链表优于数组的:
A. 插入与删除的操作。如果数组的中间插入一个元素,那么这个元素后的所有元素的内存地址都要往后移动。删除的话同理。只有对数据的最后一个元素进行插入删除操作时,才比较快。链表只需要更改有必要更改的节点内的节点信息就够了。并不需要更改节点的内存地址。
B. 内存地址的利用率方面。不管你内存里还有多少空间,如果没办法一次性给出数组所需的要空间,那就会提示内存不足,磁盘空间整理的原因之一在这里。而链表可以是分散的空间地址。
C. 链表的扩展性比数组好。因为一个数组建立后所占用的空间大小就是固定的,如果满了就没法扩展,只能新建一个更大空间的数组;而链表不是固定的,可以很方便的扩展。
笔试面试中常用的10个算法
摘要:面试也是一门学问,在面试之前做好充分的准备则是成功的必须条件,而程序员在代码面试时,常会遇到编写算法的相关问题,比如排序、二叉树遍历等等。
在程序员的职业生涯中,算法亦算是一门基础课程,尤其是在面试的时候,很多公司都会让程序员编写一些算法实例,例如快速排序、二叉树查找等等。
本文总结了程序员在代码面试中最常遇到的10大算法类型,想要真正了解这些算法的原理,还需程序员们花些功夫。
1.String/Array/Matrix
在Java中,String是一个包含char数组和其它字段、方法的类。如果没有IDE自动完成代码,下面这个方法大家应该记住:
1
2
3
4
5
6
7
8
9
10
|
toCharArray()
//get char array of a String
Arrays.sort()
//sort an array
Arrays.toString(
char
[] a)
//convert to string
charAt(
int
x)
//get a char at the specific index
length()
//string length
length
//array size
substring(
int
beginIndex)
substring(
int
beginIndex,
int
endIndex)
Integer.valueOf()
//string to integer
String.valueOf()/integer to string
|
String/arrays很容易理解,但与它们有关的问题常常需要高级的算法去解决,例如动态编程、递归等。
下面列出一些需要高级算法才能解决的经典问题:
Evaluate Reverse Polish Notation
Longest Palindromic Substring
单词分割
字梯
Median of Two Sorted Arrays
正则表达式匹配
合并间隔
插入间隔
Two Sum
3Sum
4Sum
3Sum Closest
String to Integer
合并排序数组
Valid Parentheses
实现strStr()
Set Matrix Zeroes
搜索插入位置
Longest Consecutive Sequence
Valid Palindrome
螺旋矩阵
搜索一个二维矩阵
旋转图像
三角形
Distinct Subsequences Total
Maximum Subarray
删除重复的排序数组
删除重复的排序数组2
查找没有重复的最长子串
包含两个独特字符的最长子串
Palindrome Partitioning
2.链表
在Java中实现链表是非常简单的,每个节点都有一个值,然后把它链接到下一个节点。
1
2
3
4
5
6
7
8
9
|
class
Node {
int
val;
Node next;
Node(
int
x) {
val = x;
next =
null
;
}
}
|
比较流行的两个链表例子就是栈和队列。
栈(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
|
class
Stack{
Node top;
public
Node peek(){
if
(top !=
null
){
return
top;
}
return
null
;
}
public
Node pop(){
if
(top ==
null
){
return
null
;
}
else
{
Node temp =
new
Node(top.val);
top = top.next;
return
temp;
}
}
public
void
push(Node n){
if
(n !=
null
){
n.next = top;
top = n;
}
}
}
|
队列(Queue)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class
Queue{
Node first, last;
public
void
enqueue(Node n){
if
(first ==
null
){
first = n;
last = first;
}
else
{
last.next = n;
last = n;
}
}
public
Node dequeue(){
if
(first ==
null
){
return
null
;
}
else
{
Node temp =
new
Node(first.val);
first = first.next;
return
temp;
}
}
}
|
值得一提的是,Java标准库中已经包含一个叫做Stack的类,链表也可以作为一个队列使用(add()和remove())。(链表实现队列接口)如果你在面试过程中,需要用到栈或队列解决问题时,你可以直接使用它们。
在实际中,需要用到链表的算法有:
插入两个数字
重新排序列表
链表周期
Copy List with Random Pointer
合并两个有序列表
合并多个排序列表
从排序列表中删除重复的
分区列表
LRU缓存
3.树&堆
这里的树通常是指二叉树。
1
2
3
4
5
|
class
TreeNode{
int
value;
TreeNode left;
TreeNode right;
}
|
下面是一些与二叉树有关的概念:
二叉树搜索: 对于所有节点,顺序是:left children <= current node <= right children;
平衡vs.非平衡: 它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树;
满二叉树: 除最后一层无任何子节点外,每一层上的所有结点都有两个子结点;
完美二叉树(Perfect Binary Tree): 一个满二叉树,所有叶子都在同一个深度或同一级,并且每个父节点都有两个子节点;
完全二叉树: 若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
堆(Heap)是一个基于树的数据结构,也可以称为优先队列( PriorityQueue),在队列中,调度程序反复提取队列中第一个作业并运行,因而实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权。堆即为解决此类问题设计的一种数据结构。
下面列出一些基于二叉树和堆的算法:
二叉树前序遍历
二叉树中序遍历
二叉树后序遍历
字梯
验证二叉查找树
把二叉树变平放到链表里
二叉树路径和
从前序和后序构建二叉树
把有序数组转换为二叉查找树
把有序列表转为二叉查找树
最小深度二叉树
二叉树最大路径和
平衡二叉树
4.Graph
与Graph相关的问题主要集中在深度优先搜索和宽度优先搜索。深度优先搜索非常简单,你可以从根节点开始循环整个邻居节点。下面是一个非常简单的宽度优先搜索例子,核心是用队列去存储节点。
![](http://img.bbs.csdn.net/upload/201404/24/1398320596_556431.jpg)
第一步,定义一个GraphNode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class
GraphNode{
int
val;
GraphNode next;
GraphNode[] neighbors;
boolean
visited;
GraphNode(
int
x) {
val = x;
}
GraphNode(
int
x, GraphNode[] n){
val = x;
neighbors = n;
}
public
String toString(){
return
"value: "
+
this
.val;
}
}
|
第二步,定义一个队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class
Queue{
GraphNode first, last;
public
void
enqueue(GraphNode n){
if
(first ==
null
){
first = n;
last = first;
}
else
{
last.next = n;
last = n;
}
}
public
GraphNode dequeue(){
if
(first ==
null
){
return
null
;
}
else
{
GraphNode temp =
new
GraphNode(first.val, first.neighbors);
first = first.next;
return
temp;
}
}
}
|
第三步,使用队列进行宽度优先搜索
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
|
public
class
GraphTest {
public
static
void
main(String[] args) {
GraphNode n1 =
new
GraphNode(
1
);
GraphNode n2 =
new
GraphNode(
2
);
GraphNode n3 =
new
GraphNode(
3
);
GraphNode n4 =
new
GraphNode(
4
);
GraphNode n5 =
new
GraphNode(
5
);
n1.neighbors =
new
GraphNode[]{n2,n3,n5};
n2.neighbors =
new
GraphNode[]{n1,n4};
n3.neighbors =
new
GraphNode[]{n1,n4,n5};
n4.neighbors =
new
GraphNode[]{n2,n3,n5};
n5.neighbors =
new
GraphNode[]{n1,n3,n4};
breathFirstSearch(n1,
5
);
}
public
static
void
breathFirstSearch(GraphNode root,
int
x){
if
(root.val == x)
System.out.println(
"find in root"
);
Queue queue =
new
Queue();
root.visited =
true
;
queue.enqueue(root);
while
(queue.first !=
null
){
GraphNode c = (GraphNode) queue.dequeue();
for
(GraphNode n: c.neighbors){
if
(!n.visited){
System.out.print(n +
" "
);
n.visited =
true
;
if
(n.val == x)
System.out.println(
"Find "
+n);
queue.enqueue(n);
}
}
}
}
}
|
输出结果:
value: 2 value: 3 value: 5 Find value: 5
value: 4
实际中,基于Graph需要经常用到的算法:
5.排序
不同排序算法的时间复杂度,大家可以到wiki上查看它们的基本思想。
BinSort、Radix Sort和CountSort使用了不同的假设,所有,它们不是一般的排序方法。
下面是这些算法的具体实例,另外,你还可以阅读:Java开发者在实际操作中是如何排序的。
归并排序
快速排序
插入排序
6.递归和迭代
下面通过一个例子来说明什么是递归。
问题:
这里有n个台阶,每次能爬1或2节,请问有多少种爬法?
步骤1:查找n和n-1之间的关系
为了获得n,这里有两种方法:一个是从第一节台阶到n-1或者从2到n-2。如果f(n)种爬法刚好是爬到n节,那么f(n)=f(n-1)+f(n-2)。
步骤2:确保开始条件是正确的
f(0) = 0;
f(1) = 1;
1
2
3
4
5
|
public
static
int
f(
int
n){
if
(n <=
2
)
return
n;
int
x = f(n-
1
) + f(n-
2
);
return
x;
}
|
递归方法的时间复杂度指数为n,这里会有很多冗余计算。
1
2
3
4
|
f(
5
)
f(
4
) + f(
3
)
f(
3
) + f(
2
) + f(
2
) + f(
1
)
f(
2
) + f(
1
) + f(
2
) + f(
2
) + f(
1
)
|
该递归可以很简单地转换为迭代。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public
static
int
f(
int
n) {
if
(n <=
2
){
return
n;
}
int
first =
1
, second =
2
;
int
third =
0
;
for
(
int
i =
3
; i <= n; i++) {
third = first + second;
first = second;
second = third;
}
return
third;
}
|
在这个例子中,迭代花费的时间要少些。关于迭代和递归,你可以去 这里 看看。
7.动态规划
动态规划主要用来解决如下技术问题:
通过较小的子例来解决一个实例;
对于一个较小的实例,可能需要许多个解决方案;
把较小实例的解决方案存储在一个表中,一旦遇上,就很容易解决;
附加空间用来节省时间。
上面所列的爬台阶问题完全符合这四个属性,因此,可以使用动态规划来解决:
1
2
3
4
5
6
7
8
9
10
11
12
|
public
static
int
[] A =
new
int
[
100
];
public
static
int
f3(
int
n) {
if
(n <=
2
)
A[n]= n;
if
(A[n] >
0
)
return
A[n];
else
A[n] = f3(n-
1
) + f3(n-
2
);
//store results so only calculate once!
return
A[n];
}
|
一些基于动态规划的算法:
编辑距离
最长回文子串
单词分割
最大的子数组
8.位操作
位操作符:
![](http://img.bbs.csdn.net/upload/201404/24/1398337471_969269.jpg)
从一个给定的数n中找位i(i从0开始,然后向右开始)
1
2
3
4
5
6
7
8
9
|
public
static
boolean
getBit(
int
num,
int
i){
int
result = num & (
1
<<i);
if
(result ==
0
){
return
false
;
}
else
{
return
true
;
}
}
|
例如,获取10的第二位:
1
2
3
4
|
i=
1
, n=
10
1
<<
1
=
10
1010
&
10
=
10
10
is not
0
, so
return
true
;
|
典型的位算法:
Find Single Number
Maximum Binary Gap
9.概率
通常要解决概率相关问题,都需要很好地格式化问题,下面提供一个简单的例子:
有50个人在一个房间,那么有两个人是同一天生日的可能性有多大?(忽略闰年,即一年有365天)
算法:
1
2
3
4
5
6
7
8
9
10
|
public
static
double
caculateProbability(
int
n){
double
x =
1
;
for
(
int
i=
0
; i<n; i++){
x *= (
365.0
-i)/
365.0
;
}
double
pro = Math.round((
1
-x) *
100
);
return
pro/
100
;
}
|
结果:calculateProbability(50) = 0.97
10.组合和排列
组合和排列的主要差别在于顺序是否重要。
例1:
1、2、3、4、5这5个数字,输出不同的顺序,其中4不可以排在第三位,3和5不能相邻,请问有多少种组合?
例2:
有5个香蕉、4个梨、3个苹果,假设每种水果都是一样的,请问有多少种不同的组合?
基于它们的一些常见算法
排列
排列2
排列顺序