数据结构学习笔记
数据结构
第一章 概 论
1.数据:信息的载体,能被计算机识别、存储和加工处理。
2.数据元素:数据的基本单位,可由若干个数据项组成,数据项是具有独立含义的最小标识单位。
3.数据结构:数据之间的相互关系,即数据的组织形式。
它包括:1)数据的逻辑结构,从逻辑关系上描述数据,与数据存储无关,独立于计算机;
2)数据的存储结构,是逻辑结构用计算机语言的实现,依赖于计算机语言。
3)数据的运算,定义在逻辑结构上,每种逻辑结构都有一个运算集合。常用的运算:检索/插入/删除/更新/排序。
4.数据的逻辑结构可以看作是从具体问题抽象出来的数学模型。数据的存储结构是逻辑结构用计算机语言的实现。
5.数据类型:一个值的集合及在值上定义的一组操作的总称。分为:原子类型和结构类型。
6.抽象数据类型:抽象数据的组织和与之相关的操作。优点:将数据和操作封装在一起实现了信息隐藏。
7. 抽象数据类型ADT:是在概念层上描述问题;类:是在实现层上描述问题;在应用层上操作对象(类的实例)解决问题。
8.数据的逻辑结构,简称为数据结构,有:
(1)线性结构,若结构是非空集则仅有一个开始和终端结点,并且所有结点最多只有一个直接前趋和后继。
(2)非线性结构,一个结点可能有多个直接前趋和后继。
9.数据的存储结构有:
1)顺序存储,把逻辑相邻的结点存储在物理上相邻的存储单元内。
2)链接存储,结点间的逻辑关系由附加指针字段表示。
3)索引存储,存储结点信息的同时,建立附加索引表,有稠密索引和稀疏索引。
4)散列存储,按结点的关键字直接计算出存储地址。
10.评价算法的好坏是:算法是正确的;执行算法所耗的时间;执行算法的存储空间(辅助存储空间);易于理解、编码、调试。
11.算法的时间复杂度T(n):是该算法的时间耗费,是求解问题规模n的函数。记为O(n)。
时间复杂度按数量级递增排列依次为:常数阶O(1)、对数阶O(log2n)、线性阶O(n)、线性对数阶O(nlog2n)、平方阶O(n^2)、立方阶O(n^3)、……k次方阶O(n^k)、指数阶O(2^n)。13.算法的空间复杂度S(n):是该算法的空间耗费,是求解问题规模n的函数。
12.算法衡量:是用时间复杂度和空间复杂度来衡量的,它们合称算法的复杂度。
13. 算法中语句的频度不仅与问题规模有关,还与输入实例中各元素的取值相关。
第 二 章 线 性 表
1.线性表:是由n(n≥0)个数据元素组成的有限序列。
2.线性表的基本运算有:
1)InitList(L),构造空表,即表的初始化;
2)ListLength(L),求表的结点个数,即表长;
3)GetNode(L,i),取表中第i个结点,要求1≤i≤ListLength(L);
4)LocateNode(L,x)查找L中值为x的结点并返回结点在L中的位置,有多个x则返回首个,没有则返回特殊值表示查找失败。
5)InsertList(L,x,i)在表的第i个位置插入值为x的新结点,要求1≤i≤ListLength(L)+1;
6)DeleteList(L,i)删除表的第i个位置的结点,要求1≤i≤ListLength(L);
3.顺序表:把线性表的结点按逻辑次序存放在一组地址连续的存储单元里。
4.顺序表结点的存储地址计算公式:Loc(ai)=Loc(a1)+(i-1)*C;1≤i≤n
5.顺序表上的基本运算
(1)插入
void insertlist(seqlist *L,datatype x,int i)
{
int j;
if(i<1||i>L->length+1)
error(“position error”);
if(L->length>=listsize)
error(“overflow”);
for(j=L->length-1;j>=i-1;j--)
L->data[j+1]=L->data[j]; 结点后移
L->data[i-1]=x;
L->length++;
}
在顺序表上插入要移动表的n/2结点,算法的平均时间复杂度为O(n)。
(2)删除
void delete (seqlist *L,int i)
{
int j;
if(i<1||i>L->length)
error(“position error”);
for(j=i;j<=L->length-1;j++)
L->data[j-1]=L->data[j]; 结点前移
L->length--;
}
在顺序表上删除要移动表的(n+1)/2结点,算法的平均时间复杂度为O(n)。
6.单链表:只有一个链域的链表称单链表。
在结点中存储结点值和结点的后继结点的地址,data next data是数据域,next是指针域。
(1)建立单链表。时间复杂度为O(n)。
加头结点的优点:1)链表第一个位置的操作无需特殊处理;2)将空表和非空表的处理统一。
(2)查找运算。时间复杂度为O(n)。
1) 按序号查找。
Listnode * getnode(linklist head,int i)
{
int j;
listnode *p;
p=head;j=0;
while(p->next&&j<i){
p=p->next; 指针下移
j++;
}
if(i==j)
return p;
else
return NULL;
}
2) 按值查找。
Listnode * locatenode(linklist head ,datatype key)
{
listnode *p=head->next;
while(p&&p->data!=key)
p=p->next;
return p;
}
(3)插入运算。时间复杂度为O(n)。
Void insertlist(linklist head ,datatype x, int i)
{
listnode *p;
p=getnode(head,i-1);
if(p==NULL);
error(“position error”);
s=(listnode *)malloc(sizeof(listnode));
s->data=x;
s->next=p->next;
p->next=s;
}
(4) 删除运算。时间复杂度为O(n)。
Void deletelist(linklist head ,int i)
{
listnode *p ,*r;
p=getnode(head ,i-1);
if(p==NULL||p->next==NULL)
error(“position error”);
r=p->next;
p->next=r->next;
free(r);
}
7.循环链表:是一种首尾相连的链表。特点是无需增加存储量,仅对表的链接方式修改使表的处理灵活方便。
8.空循环链表仅由一个自成循环的头结点表示。
9.很多时候表的操作是在表的首尾位置上进行,此时头指针表示的单循环链表就显的不够方便,改用尾指针rear来表示单循环链表。用头指针表示的单循环链表查找开始结点的时间是O(1),查找尾结点的时间是O(n);用尾指针表示的单循环链表查找开始结点和尾结点的时间都是O(1)。
10.在结点中增加一个指针域,prior|data|next。形成的链表中有两条不同方向的链称为双链表。
1) 双链表的前插操作。时间复杂度为O(1)。
Void dinsertbefore(dlistnode *p ,datatype x)
{
dlistnode *s=malloc(sizeof(dlistnode));
s->data=x;
s->prior=p->prior;
s->next=p;
p->prior->next=s;
p->prior=s;
}
2) 双链表的删除操作。时间复杂度为O(1)。
Void ddeletenode(dlistnode *p)
{
p->prior->next=p->next;
p->next->prior=p->prior;
free(p);
}
11.顺序表和链表的比较
1) 基于空间的考虑:顺序表的存储空间是静态分配的,链表的存储空间是动态分配的。顺序表的存储密度比链表大。因此,在线性表长度变化不大,易于事先确定时,宜采用顺序表作为存储结构。
2) 基于时间的考虑:顺序表是随机存取结构,若线性表的操作主要是查找,很少有插入、删除操作时,宜用顺序表结构。对频繁进行插入、删除操作的线性表宜采用链表。若操作主要发生在表的首尾时采用尾指针表示的单循环链表。
12.存储密度=(结点数据本身所占的存储量)/(整个结点结构所占的存储总量)
存储密度:顺序表=1,链表<1。
第 三 章 栈 和 队 列
1.栈是限制仅在表的一端进行插入和删除运算的线性表又称为后进先出表(LIFO表)。插入、删除端称为栈顶,另一端称栈底。表中无元素称空栈。
2.栈的基本运算有:
1) initstack(s),构造一个空栈;
2) stackempty(s),判栈空;
3) stackfull(s),判栈满;
4) push(s,x),进栈;
5) pop (s),退栈;
6) stacktop(s),取栈顶元素。
3.顺序栈:栈的顺序存储结构称顺序栈。
4.当栈满时,做进栈运算必定产生空间溢出,称“上溢”。 当栈空时,做退栈运算必定产生空间溢出,称“下溢”。上溢是一种错误应设法避免,下溢常用作程序控制转移的条件。
5.在顺序栈上的基本运算:
1) 置空栈。
Void initstack(seqstack *s)
{
s->top=-1;
}
2)判栈空。
int stackempty(seqstack *s)
{
return s->top==-1;
}
3)判栈满。
int stackfull(seqstack *s)
{
return s->top==stacksize-1;
}
4)进栈。
Void push(seqstack *s,datatype x)
{
if(stackfull(s))
error(“stack overflow”);
s->data[++s->top]=x;
}
5)退栈。
Datatype pop(seqstack *s)
{
if(stackempty(s))
error(“stack underflow”);
return S->data[s->top--];
}
6)取栈顶元素。
Dtatatype stacktop(seqstack *s)
{
if(stackempty(s))
error(“stack underflow”);
return S->data[s->top];
}
6.链栈:栈的链式存储结构称链栈。栈顶指针是链表的头指针。
7.链栈上的基本运算:
1) 建栈。
Void initstack(linkstack *s)
{
s->top=NULL;
}
2)判栈空。
Int stackempty (linkstack *s)
{
return s->top==NULL;
}
3) 进栈。
Void push(linkstack *s,datatype x)
{
stacknode *p=(stacknode *)malloc(sizeof(stacknode));
p->data=x;
p->next=s->top;
s->top=p;
}
4) 退栈。
Datatype pop(linksatck *s)
{
datatype x;
stacknode *p=s->top;
if(stackempty(s))
error(“stack underflow”);
x=p->data;
s->top=p->next;
free(p);
return x;
}
5) 取栈顶元素。
Datatype stacktop(linkstack *s)
{
if(stackempty(s))
error(“stack is empty”);
return s->top->data;
}
8.队列是一种运算受限的线性表,允许删除的一端称队首,允许插入的一端称队尾。队列又称为先进先出线性表,FIFO表。
9.队列的基本运算:
1) initqueue(q),置空队;
2) queueempty(q),判队空;
3) queuefull(q),判队满;
4) enqueue(q,x),入队;
5) dequeue(q),出队;
6) queuefront(q),返回队头元素。
10.顺序队列:队列的顺序存储结构称顺序队列。设置front和rear指针表示队头和队尾元素在向量空间的位置。
11.顺序队列中存在“假上溢”现象,由于入队和出队操作使头尾指针只增不减导致被删元素的空间无法利用,队尾指针超过向量空间的上界而不能入队。
12.为克服“假上溢”现象,将向量空间想象为首尾相连的循环向量,存储在其中的队列称循环队列。i=(i+1)%queuesize
13.循环队列的边界条件处理:由于无法用front==rear来判断队列的“空”和“满”。
解决的方法有:
1) 另设一个布尔变量以区别队列的空和满;
2) 少用一个元素,在入队前测试rear在循环意义下加1是否等于front;
3) 使用一个记数器记录元素总数。
14.循环队列的基本运算:
1) 置队空。
Void initqueue(cirqueue *q)
{
q->front=q->rear=0;
q->count=0;
}
2) 判队空。
Int queueempty(cirqueue *q)
{
return q->count==0;
}
3) 判队满。
Int queuefull(cirqueue *q)
{
return q->count==queuesize;
}
4) 入队。
Void enqueue(cirqueue *q ,datatype x)
{
if(queuefull(q))
error(“queue overfolw”);
q->count++;
q->data[q->rear]=x;
q->rear=(q->rear+1)%queuesize;
}
5) 出队。
Datatype dequeue(cirqueue *q)
{
datatype temp;
if(queueempty(q))
error(“queue underflow”);
temp=q->data[q->front];
q->count--;
q->front=(q->front+1)%queuesize;
return temp;
}
6) 取队头元素。
Datatype queuefront(cirqueue *q)
{
if(queueempty(q))
error(“queue is empty”);
return q->data[q->front];
}
15.链队列:队列的链式存储结构称链队列,链队列由一个头指针和一个尾指针唯一确定。
16.链队列的基本运算:
1) 建空队。
Void initqueue(linkqueue *q)
{
q->front=q->rear=NULL;
}
2) 判队空。
Int queueempty(linkqueue *q)
{
return q->front==NULL&&q->rear==NULL;
}
3) 入队。
Void enqueue(linkqueue *q,datatype x)
{
queuenode *p=(queuenode *)malloc(sizeof(queuenode));
p->data=x;
p->next=NULL;
if(queueempty(q))
q-front=q->rear=p;
else{
q->rear->next=p;
q->rear=p;
}
}
4) 出队。
Datatype dequeue(linkqueue *q)
{
datatype x;
queuenode *p;
if(queueempty(q))
error(“queue is underflow”);
p=q->front;
x=p->data;
q->front=p->next;
if(q->rear==p) q->rear=NULL;
free(p);
return x;
}
5) 取队头元素。
Datatype queuefront(linkqueue *q)
{
if(queueempty(q))
error(“queue is empty”);
return q->front->data;
}
第 四 章 串
1.串:是由零个或多个字符组成的有限序列;包含字符的个数称串的长度;
2.空串:长度为零的串称空串; 空白串:由一个或多个空格组成的串称空白串;
子串:串中任意个连续字符组成的子序列称该串的子串; 主串:包含子串的串称主串;
子串的首字符在主串中首次出现的位置定义为子串在主串中的位置;
3.空串是任意串的子串; 任意串是自身的子串;
串常量在程序中只能引用但不能改变其值; 串变量取值可以改变;
4.串的基本运算
1) int strlen(char *s);求串长。
2) char *strcpy(char * to,char * from);串复制。
3) char *strcat(char * to,char * from);串联接。
4) int strcmp(char *s1,char *s2);串比较。
5) char *strchr(char *s,char c);字符定位。
5.串的存储结构:
(1)串的顺序存储:串的顺序存储结构称顺序串。按存储分配不同分为:
1) 静态存储分配的顺序串:
直接用定长的字符数组定义,以“\0”表示串值终结。
#define maxstrsize 256
typedef char seqstring[maxstrsize];
seqstring s;
不设终结符,用串长表示。
Typedef struct{
Char ch[maxstrsize];
Int length;
}seqstring;
以上方式的缺点是:串值空间大小是静态的,难以适应插入、链接等操作。
2) 动态存储分配的顺序串:
简单定义:typedef char * string;
复杂定义:typedef struct{
char *ch;
int length;
}hstring;
(2)串的链式存储:串的链式存储结构称链串。链串由头指针唯一确定。类型定义:
typedef struct node{
char data;
struct node *next;
}linkstrnode;
typedef linkstrnode *linkstring;
linkstring s;
将结点数据域存放的字符个数定义为结点的大小。结点大小不为1的链串类型定义:
#define nodesize 80
typedef struct node{
char data[nodesize];
struct node * next;
}linkstrnode;
6.串运算的实现
(1)顺序串上的子串定位运算。
1)子串定位运算又称串的模式匹配或串匹配。主串称目标串;子串称模式串。
2)朴素的串匹配算法。时间复杂度为O(n^2)。比较的字符总次数为(n-m+1)m。
Int naivestrmatch(seqstring t,seqstring p)
{
int i,j,k;
int m=p.length;
int n=t.length;
for(i=0;i<=n-m;i++){
j=0;k=i;
while(j<m&&t.ch[k]==p.ch[j]){
j++;k++;
}
if (j==m) return i;
}
return –1;
}
(2)链串上的子串定位运算。时间复杂度为O(n^2)。比较的字符总次数为(n-m+1)m。
Linkstrnode * lilnkstrmatch(linkstring T, linkstring P)
{
linkstrnode *shift, *t, *p;
shift=T;
t=shift;p=P;
while(t&&p){
if(t->data==p->data){
t=t->next;
p=p->next;
}
else{
shift=shift->next;
t=shift;
p=P;
}
}
if(p==NULL)
return shift;
else
return NULL;
}
第 五 章 多 维 数 组 和 广 义 表
1.多维数组:一般用顺序存储的方式表示数组。
2.常用方式有:1)行优先顺序,将数组元素按行向量排列;
2)列优先顺序,将数组元素按列向量排列。
3.计算地址的函数:LOC(Aij)=LOC(Ac1c2)+((i-c1)*(d2-c2+1)+j-c2)*d
4.矩阵的压缩存储:为多个非零元素分配一个存储空间;对零元素不分配存储空间。
(1) 对称矩阵:在一个n阶的方阵A中,元素满足Aij=Aji 0<=i,j<=n-1;称为对称矩阵。
元素的总数为:n(n+1)/2;
设:I=i或j中大的一个数;J=i或j中小的一个数;
则:k=I*(I+1)/2+J;
地址计算:LOC(Aij)=LOC(sa[k])=LOC(sa[0])+k*d= LOC(sa[0])+ (I*(I+1)/2+J )*d
(2)三角矩阵:以主对角线划分,三角矩阵有上三角和下三角;上三角的主对角线下元素均为常数c;下三角的主对角线上元素均为常数c。
元素总数为:(n(n+1)/2)+1;
以行优先顺序存放的Aij与SA[k]的关系:
上三角阵:k=i*(2n-i+1)/2+j-i;
下三角阵:k=i*(i+1)/2+j;
(3)对角矩阵:所有的非零元素集中在以主对角线为中心的带状区域,相邻两侧元素均为零。|i-j|>(k-1)/2
以行优先顺序存放的Aij与SA[k]的关系:k=2i+j;
5.稀疏矩阵:当矩阵A中有非零元素S个,且S远小于元素总数时,称为稀疏矩阵。
对其压缩的方法有顺序存储和链式存储。
(1)三元组表:将表示稀疏矩阵的非零元素的三元组(行号、列号、值)按行或列优先的顺序排列得到的一个结点均是三元组的线性表,将该表的线性存储结构称为三元组表。其类型定义:
#define maxsize 10000
typedef int datatype;
typedef struct{
int i,j;
datatype v;
}trituplenode;
typedef struct{
trituplenode data[maxsize];
int m,n,t;
}tritupletable;
(2)带行表的三元组表:在按行优先存储的三元组表中加入一个行表记录每行的非零元素在三元组表中的起始位置。类型定义:
#define maxrow 100
typedef struct{
tritulpenode data[maxsize];
int rowtab[maxrow];
int m, n, t;
}rtritulpetable;
6.广义表:是线性表的推广,广义表是n个元素的有限序列,元素可以是原子或一个广义表,记为LS。
7.若元素是广义表称它为LS的子表。若广义表非空,则第一个元素称表头,其余元素称表尾。
8.表的深度是指表展开后所含括号的层数。
9.把与树对应的广义表称为纯表,它限制了表中成分的共享和递归;
10.允许结点共享的表称为再入表;
11.允许递归的表称为递归表;
12.相互关系:线性表∈纯表∈再入表∈递归表;
13.广义表的特殊运算:1)取表头head(LS);2)取表尾tail(LS);
第 六 章 树
1.树:是n个结点的有限集T,T为空时称空树,否则满足:
1)有且仅有一个特定的称为根的结点;
2)其余结点可分为m个互不相交的子集,每个子集本身是一棵树,并称为根的子树。
2.树的表示方法:1)树形表示法;2)嵌套集合表示法;3)凹入表表示法;4)广义表表示法;
3.一个结点拥有的子树数称为该结点的度;一棵树的度是指树中结点最大的度数。
4.度为零的结点称叶子或终端结点;度不为零的结点称分支结点或非终端结点
5.根结点称开始结点,根结点外的分支结点称内部结点;
6.树中某结点的子树根称该结点的孩子;该结点称为孩子的双亲;
7.树中存在一个结点序列K1,K2,…Kn,使Ki为Ki+1的双亲,则称该结点序列为K1到Kn的路径或道路;
8.树中结点K到Ks间存在一条路径,则称K是Ks的祖先,Ks是K的子孙;
9.结点的层数从根算起,若根的层数为1,则其余结点层数是其双亲结点层数加1;双亲在同一层的结点互为堂兄弟;树中结点最大层数称为树的高度或深度;
10.树中每个结点的各个子树从左到右有次序的称有序树,否则称无序树;
11.森林是m棵互不相交的树的集合。
12.二叉树:是n个结点的有限集,它或为空集,或由一个根结点及两棵互不相交的、分别称为该根的左子树和右子树的二叉树组成。
13.二叉树不是树的特殊情况,这是两种不同的数据结构;它与无序树和度为2的有序树不同。
14.二叉树的性质:
1) 二叉树第i层上的结点数最多为2^(i-1);
2) 深度为k的二叉树至多有2^k-1个结点;
3) 在任意二叉树中,叶子数为n0,度为2的结点数为n2,则n0=n2+1;
15.满二叉树是一棵深度为k的且有2^k-1个结点的二叉树;
16.完全二叉树是至多在最下两层上结点的度数可以小于2,并且最下层的结点集中在该层最左的位置的二叉树;
17.具有N个结点的完全二叉树的深度为log2N取整加1;
18.二叉树的存储结构
(1)顺序存储结构:把一棵有n个结点的完全二叉树,从树根起自上而下、从左到右对所有结点编号,然后依次存储在一个向量b[0~n]中,b[1~n]存放结点,b[0]存放结点总数。
各个结点编号间的关系:
1) i=1是根结点;i>1则双亲结点是i/2取整;
2) 左孩子是2i, 右孩子是2i+1;(要小于n)
3) i>(n/2取整)的结点是叶子;
4) 奇数没有右兄弟,左兄弟是i-1;
5) 偶数没有左兄弟,右兄弟是i+1;
(2)链式存储结构
结点的结构为:lchild|data|rchild ;相应的类型说明:
typedef char data;
typedef struct node{
datatype data;
structnode *lchild , *rchild;
}bintnode;
typedef bintnode * bintree;
19.在二叉树中所有类型为bintnode的结点和一个指向开始结点的bintree类型的头指针构成二叉树的链式存储结构称二叉链表。
20.二叉链表由根指针唯一确定。在n个结点的二叉链表中有2n个指针域,其中n+1个为空。
21.二叉树的遍历方式有:前序遍历、中序遍历、后序遍历。时间复杂度为O(n)。
22.线索二叉树:利用二叉链表中的n+1个空指针域存放指向某种遍历次序下的前趋和后继结点的指针,这种指针称线索。加线索的二叉链表称线索链表。相应二叉树称线索二叉树。
23.线索链表结点结构:lchild|ltag|data|rtag|rchild;ltag=0,lchild是指向左孩子的指针;ltag=1,lchild是指向前趋的线索;rtag=0,rchild是指向右孩子的指针;rtag=1,rchild是指向后继的线索;
24.查找*p在指定次序下的前趋和后继结点。算法的时间复杂度为O(h)。线索对查找前序前趋和后序后继帮助不大。
25.遍历线索二叉树。时间复杂度为O(n)。
26.树、森林与二叉树的转换
(1)树、森林与二叉树的转换
1)树与二叉树的转换:1}所有兄弟间连线;2}保留与长子的连线,去除其它连线。该二叉树的根结点的右子树必为空。
2)森林与二叉树的转换:1}将所有树转换成二叉树;2}将所有树根连线。
(2)二叉树与树、森林的转换。是以上的逆过程。
27.树的存储结构
(1)双亲链表表示法:为每个结点设置一个parent指针,就可唯一表示任何一棵树。Data|parent
(2)孩子链表表示法:为每个结点设置一个firstchild指针,指向孩子链表头指针,链表中存放孩子结点序号。Data|firstchild。
(3双亲孩子链表表示法:将以上方法结合。Data|parent|firstchild
(4)孩子兄弟链表表示法:附加两个指向左孩子和右兄弟的指针。Leftmostchild|data|rightsibling
28.树和森林的遍历:前序遍历一棵树等价于前序遍历对应二叉树;后序遍历等价于中序遍历对应二叉树。
29.最优二叉树(哈夫曼树):树的路径长度是从树根到每一结点的路径长度之和。将树中的结点赋予实数称为结点的权。
30.结点的带权路径是该结点的路径长度与权的乘积。树的带权路径长度又称树的代价,是所有叶子的带权路径长度之和。
31.带权路径长度最小的二叉树称最优二叉树(哈夫曼树)。
32.具有2n-1个结点其中有n个叶子,并且没有度为1的分支结点的树称为严格二叉树。
33.哈夫曼编码
34.对字符集编码时,要求字符集中任一字符的编码都不是其它字符的编码前缀,这种编码称前缀码。
35.字符出现频度与码长乘积之和称文件总长;字符出现概率与码长乘积之和称平均码长;
36.使文件总长或平均码长最小的前缀码称最优前缀码
37.利用哈夫曼树求最优前缀码,左为0,右为1。编码平均码长最小;没有叶子是其它叶子的祖先,不可能出现重复前缀。
第 七 章 图
1.图:图G是由顶点集V和边集E组成,顶点集是有穷非空集,边集是有穷集;
2.G中每条边都有方向称有向图;有向边称弧;边的始点称弧尾;边的终点称弧头;G中每条边都没有方向的称无向图。
3.顶点n与边数e的关系:无向图的边数e介于0~n(n-1)/2之间,有n(n-1)/2条边的称无向完全图;有向图的边数e介于0~n(n-1)之间,有n(n-1)条边的称有向完全图;
4.无向图中顶点的度是关联与顶点的边数;有向图中顶点的度是入度与出度的和。
所有图均满足:所有顶点的度数和的一半为边数。
5.图G(V,E),如V’是V的子集,E’是E的子集,且E’中关联的顶点均在V’中,则G’(V’,E’)是G的子图。
6.在有向图中,从顶点出发都有路径到达其它顶点的图称有根图;
7.在无向图中,任意两个顶点都有路径连通称连通图;极大连通子图称连通分量;
8.在有向图中,任意顺序两个顶点都有路径连通称强连通图;极大连通子图称强连通分量;
9.将图中每条边赋上权,则称带权图为网络。
10.图的存储结构:
(1)邻接矩阵表示法:邻接矩阵是表示顶点间相邻关系的矩阵。n个顶点就是n阶方阵。
无向图是对称矩阵;有向图行是出度,列是入度。
(2)邻接表表示法:对图中所有顶点,把与该顶点相邻接的顶点组成一个单链表,称为邻接表,adjvex|next,如要保存顶点信息加入data;对所有顶点设立头结点,vertex|firstedge,并顺序存储在一个向量中;vertex保存顶点信息,firstedge保存邻接表头指针。
11.邻接矩阵表示法与邻接表表示法的比较:
1) 邻接矩阵是唯一的,邻接表不唯一;
2) 存储稀疏图用邻接表,存储稠密图用邻接矩阵;
3) 求无向图顶点的度都容易,求有向图顶点的度邻接矩阵较方便;
4) 判断是否是图中的边,邻接矩阵容易,邻接表最坏时间为O(n);
5) 求边数e,邻接矩阵耗时为O(n^2),与e无关,邻接表的耗时为O(e+n);
12.图的遍历:
(1)图的深度优先遍历:类似与树的前序遍历。按访问顶点次序得到的序列称DFS序列。
对邻接表表示的图深度遍历称DFS,时间复杂度为O(n+e); 对邻接矩阵表示的图深度遍历称DFSM,时间复杂度为O(n^2);
(2)图的广度优先遍历:类似与树的层次遍历。按访问顶点次序得到的序列称BFS序列。
对邻接表表示的图广度遍历称BFS,时间复杂度为O(n+e); 对邻接矩阵表示的图广度遍历称BFSM,时间复杂度为O(n^2);
13. 将没有回路的连通图定义为树称自由树。
14.生成树:连通图G的一个子图若是一棵包含G中所有顶点的树,该子图称生成树。
有DFS生成树和BFS生成树,BFS生成树的高度最小。
非连通图生成的是森林。
15.最小生成树:将权最小的生成树称最小生成树。(是无向图的算法)
(1)普里姆算法:
1) 确定顶点S、初始化候选边集T[0~n-2];formvex|tovex|lenght
2) 选权值最小的T[i]与第1条记录交换;
3) 从T[1]中将tovex取出替换以下记录的fromvex计算权;若权小则替换,否则不变;
4) 选权值最小的T[i]与第2条记录交换;
5) 从T[2]中将tovex取出替换以下记录的fromvex计算权;若权小则替换,否则不变;
6) 重复n-1次。
初始化时间是O(n),选轻边的循环执行n-1-k次,调整轻边的循环执行n-2-k;算法的时间复杂度为O(n^2),适合于稠密图。
(2)克鲁斯卡尔算法:
1) 初始化确定顶点集和空边集;对原边集按权值递增顺序排序;
2) 取第1条边,判断边的2个顶点是不同的树,加入空边集,否则删除;
3) 重复e次。
对边的排序时间是O(elog2e);初始化时间为O(n);执行时间是O(log2e);算法的时间复杂度为O(elog2e),适合于稀疏图。
16. 路径的开始顶点称源点,路径的最后一个顶点称终点;
17.单源最短路径问题:已知有向带权图,求从某个源点出发到其余各个顶点的最短路径;
18.单目标最短路径问题:将图中每条边反向,转换为单源最短路径问题;
19.单顶点对间最短路径问题:以分别对不同顶点转换为单源最短路径问题;
20.所有顶点对间最短路径问题:分别对图中不同顶点对转换为单源最短路径问题;
21.迪杰斯特拉算法:
1) 初始化顶点集S[i],路径权集D[i],前趋集P[i];
2) 设置S[s]为真,D[s]为0;
3) 选取D[i]最小的顶点加入顶点集;
4) 计算非顶点集中顶点的路径权集;
5) 重复3)n-1次。
算法的时间复杂度为O(n^2)。
22.拓扑排序:对一个有向无环图进行拓扑排序,是将图中所有顶点排成一个线性序列,满足弧尾在弧头之前。这样的线性序列称拓扑序列。
(1)无前趋的顶点优先:总是选择入度为0的结点输出并删除该顶点的所有边。
设置各个顶点入度时间是O(n+e),设置栈或队列的时间是O(n),算法时间复杂度为O(n+e)。
(2)无后继的顶点优先:总是选择出度为0的结点输出并删除该顶点的所有边。
设置各个顶点出度时间是O(n+e),设置栈或队列的时间是O(n),算法时间复杂度为O(n+e)。
求得的是逆拓扑序列。
第 八 章 排 序
1.文件:由一组记录组成,记录有若干数据项组成,唯一标识记录的数据项称关键字;
2.排序是将文件按关键字的递增(减)顺序排列;
3.排序文件中有相同的关键字时,若排序后相对次序保持不变的称稳定排序,否则称不稳定排序;
4.在排序过程中,文件放在内存中处理不涉及数据的内、外存交换的称内排序,反之称外排序;
5.排序算法的基本操作:1)比较关键字的大小;2)改变指向记录的指针或移动记录本身。
6.评价排序方法的标准:1)执行时间;2)所需辅助空间,辅助空间为O(1)称就地排序;另要注意算法的复杂程度。
7.若关键字类型没有比较运算符,可事先定义宏或函数表示比较运算。
8.插入排序
(1)直接插入排序
算法中引入监视哨R[0]的作用是:1)保存R[i]的副本;2)简化边界条件,防止循环下标越界。
关键字比较次数最大为(n+2)(n-1)/2;记录移动次数最大为(n+4)(n-1)/2;
算法的最好时间是O(n);最坏时间是O(n^2);平均时间是O(n^2);是一种就地的稳定的排序;
(2)希尔排序
实现过程:是将直接插入排序的间隔变为d。d的取值要注意:1)最后一次必为1;2)避免d值互为倍数;
关键字比较次数最大为n^1.25;记录移动次数最大为1.6n^1.25;
算法的平均时间是O(n^1.25);是一种就地的不稳定的排序;
9.交换排序
(1)冒泡排序
实现过程:从下到上相邻两个比较,按小在上原则扫描一次,确定最小值,重复n-1次。
关键字比较次数最小为n-1、最大为n(n-1)/2;记录移动次数最小为0,最大为3n(n-1)/2;
算法的最好时间是O(n);最坏时间是O(n^2);平均时间是O(n^2);是一种就地的稳定的排序;
(2)快速排序
实现过程:将第一个值作为基准,设置i,j指针交替从两头与基准比较,有交换后,交换j,i。i=j时确定基准,并以其为界限将序列分为两段。重复以上步骤。
关键字比较次数最好为nlog2n+nC(1)、最坏为n(n-1)/2;
算法的最好时间是O(nlog2n);最坏时间是O(n^2);平均时间是O(nlog2n);辅助空间为O(log2n);是一种不稳定排序;
10.选择排序
(1)直接选择排序
实现过程:选择序列中最小的插入第一位,在剩余的序列中重复上一步,共重复n-1次。
关键字比较次数为n(n-1)/2;记录移动次数最小为0,最大为3(n-1);
算法的最好时间是O(n^2);最坏时间是O(n^2);平均时间是O(n^2);是一种就地的不稳定的排序;
(2)堆排序
实现过程:把序列按层次填入完全二叉树,调整位置使双亲大于或小于孩子,建立初始大根或小根堆,调整树根与最后一个叶子的位置,排除该叶子重新调整位置。
算法的最好时间是O(nlog2n);最坏时间是O(nlog2n);平均时间是O(nlog2n);是一种就地的不稳定排序;
11.归并排序
实现过程:将初始序列分为2个一组,最后单数轮空,对每一组排序后作为一个单元,对2个单元排序,直到结束。
算法的最好时间是O(nlog2n);最坏时间是O(nlog2n);平均时间是O(nlog2n);辅助空间为O(n);是一种稳定排序;
12.分配排序
(1)箱排序
实现过程:按关键字的取值范围确定箱子的个数,将序列按关键字放入箱中,输出非空箱的关键字。
在桶内分配和收集,及对各桶进行插入排序的时间为O(n),算法的期望时间是O(n),最坏时间是O(n^2)。
(2)基数排序
实现过程:按基数设置箱子,对关键字从低位到高位依次进行箱排序。
算法的最好时间是O(d*n+d*rd);最坏时间是O(d*n+d*rd);平均时间是O(d*n+d*rd);辅助空间O(n+rd);是一种稳定排序;
13.各种内部排序方法的比较和选择:
(1)按平均时间复杂度分为:
1) 平方阶排序:直接插入、直接选择、冒泡排序;
2) 线性对数阶:快速排序、堆排序、归并排序;
3) 指数阶:希尔排序;
4) 线性阶:箱排序、基数排序。
(2)选择合适排序方法的因素:
1)待排序的记录数;2)记录的大小;3)关键字的结构和初始状态;4)对稳定性的要求;
5)语言工具的条件;6)存储结构; 7)时间和辅助空间复杂度。
(3)结论:
1) 若规模较小可采用直接插入或直接选择排序;
2) 若文件初始状态基本有序可采用直接插入、冒泡或随机快速排序;
3) 若规模较大可采用快速排序、堆排序或归并排序;
4) 任何借助于比较的排序,至少需要O(nlog2n)的时间,箱排序和基数排序只适用于有明显结构特征的关键字;
5) 有的语言没有提供指针及递归,使归并、快速、基数排序算法复杂;
6) 记录规模较大时为避免大量移动记录可用链表作为存储结构,如插入、归并、基数排序,但快速、堆排序在链表上难以实现,可提取关键字建立索引表,然后对索引表排序。
第 九 章 查 找
1.查找的同时对表做修改操作(如插入或删除)则相应的表称之为动态查找表,否则称之为静态查找表。
2.衡量一个查找算法次序优劣的标准是在查找过程中对关键字需要执行的平均比较次数(即平均查找长度ASL).
3.线性表上进行查找的方法主要有三种:顺序查找、二分查找和分块查找。
(1)顺序查找的算法基本思想:是从表的一端开始顺序扫描线性表,依次将扫描到的结点关键字与给定值K比较,若当前扫描到的结点关键字与k相等则查找成功;若扫描结束后,仍未找到关键字等于K的结点,则查找失败。
1)顺序查找方法可用链式存储结构和顺序存储结构实现。
2)在顺序存储结构的顺序查找算法中所设的哨兵是为了简化循环的边界条件而引入的附加结点(元素),其作用是使for循环中省去判定防止下标越界的条件从而节省了比较的时间。
3)在等概率情况下,查找成功时其平均查找长度约为表长的一半(n+1)/2.查找失败的话其平均查找长度为n+1.
(2)二分查找(又称折半查找),它的算法思想:是对一有序表中的元素,从初始的查找区间开始,每经过一次与当前查找区间的中点位置上的结点关键字进行比较,若相等,则查找成功,否则,当前查找区间的缩小一半,按k值大小在某半个区间内重复相同的步骤进行查找,直到查找成功或失败为止。
1)二分查找在等概率的情况下查找成功的平均查找长度ASL为lg(n+1)-1,在查找失败时所需比较的关键字个数不超过判定树的深度,最坏情况下查找成功的比较次数也不超过判定树的深度┌lg(n+1)┐(不小于lg(n+1)的最小整数)
2)二分查找只适用于顺序存储结构而不能用链式存储结构实现。因为链表无法进行随机访问,如果要访问链表的中间结点,就必须先从头结点开始进行依次访问,这就要浪费很多时间,还不如进行顺序查找,而且,用链存储结构将无法判定二分的过程是否结束,因此无法用链表实现二分查找。
(3)分块查找(又称索引顺序查找)的基本思想:是将原表分成若干块,各块内部不一定有序,但表中的块是"分块有序"的,并抽取各块中的最大关键字及其起始位置建立索引表。因为索引表是有序的,分块查找就是先用二分查找或顺序查找确定待查结点在哪一块,然后在已确定的块中进行顺序查找(不能用二分查找,因为块内是无序的)。分块查找实际上是两次查找过程,它的算法效率介与顺序查找和二分查找之间。
4.以上三种查找方法的比较如下表:
查找算法 存储结构 优点 缺点 适用于
顺序查找 顺序结构
链表结构 算法简单且对表的结构无任何要求 查找效率低 n较小的表的查找和查找较少但改动较多的表(用链表作存储结构)
二分查找 顺序结构 查找效率高 关键字要有序且只能用顺序存储结构实现 特别适用于一经建立就很少改动又经常需要查找的线性表
分块查找 顺序结构
链表 在表中插入或删除记录时就只要在该记录所属块内操作,因为块内记录的存放是随意的,所以插入和删除比较容易 要增加一个辅助数组的存储空间,并要进行将初始表分块排序运算 适用于有分块特点的记录,如一个学校的学生登记表可按系号或班号分块。
5.树的查找:以树做为表的组织形式有一个好处,就是可以实现对动态查找表进行高效率的查找。这里讲到了二叉排序树和B-树,以及在这些树表上进行查找和修改操作的方法。
6.二叉排序树(BST)又称二叉查找树,其定义是:二叉排序树要或者是空树或者满足如下性质的二叉树:
1)若它的左子树非空,则左子树上所有结点的值均小于根结点的值;
2)若它的右子树非空,则右子树上所有结点的值均大于根结点的值;
3)左、右子树本身又是一棵二叉排序树。
(1)二叉排序树实际上是满足BST性质的二叉树。
(2)二叉排序树的插入、建立的算法平均时间性能是O(nlgn),但其执行时间约为堆排序的2至3倍。二叉排序树的删除操作可分三种情况进行处理:
1)*P是叶子,则直接删除*P,即将*P的双亲*parent 中指向*P的指针域置空即可。
2)*P只有一个孩子*child,此时只需将*child和*p的双亲直接连接就可删去*p.
3)*p有两个孩子,则将操作转换成删除*p结点的中序后继,在删去它之前把这个结点的数据复制到原来要删的结点位置上就完成了删除。
(3)二叉排序树上的查找和二分查找类似,它的关键字比较次数不超过树的深度。在最好的情况下,二叉排序树在生成的过程中比较匀称,此时的叉排序树是平衡的二叉树(也就是树中任一结点的左右子树的高度大致相同),它的高度约为1.44lgn,完全平衡的二叉树高度约为lgn.在最坏的情况下,输入的实例产生的二叉排序树的高度将达到O(n),这种情况应当避免。
7.关于B-树(多路平衡查找树)。它适合在磁盘等直接存取设备上组织动态的查找表,是一种外查找算法。
B树的阶是指B-树的度数,B-树的结点具有k个孩子时,该结点必有k-1(k>=2)个关键字。
实际上B-树是二叉排序树的推广,它就是一棵m叉树,且满足四个性质,这些性质与二叉排序树有相似之处,请仔细理解之。
8.上面的几种查找方法均是建立在比较关键字的基础上,因此它们的平均和最坏情况下所需的比较次数的下界是lgn+O(1).
9.散列技术:可以无需任何比较就找到待查关键字,其查找的期望时间为O(1).
散列表的概念:就是将所有可能出现的关键字的集合U(全集)映射到一个表T[0..m-1]的下标集上,这个表就是散列表。
10.而关键字与这个表地址之间以什么样的关系发生联系呢,这就要通过一个函数来建立,这个函数是以U中的关键字为自变量,以相应结点的存储地址为函数值,它就称为散列函数。将结点按其关键字的散列地址存储到散列表的过程称为散列。
11.根据某种散列函数,一个关键字的散列函数值是唯一的,但是有可能两个或多个不同关键字的函数值是相同的,这时就会把几个结点存储到同一个表位置上,这时就造成冲突(或碰撞)现象,这两个关键字称为该散列函数的同义词。
要完全(不是"安全")避免冲突需满足两个条件,一是关键字集合U不大于散列表长m,另一个是选择合适的散列函数,如果用h(ki)=0)这样的函数的话,看看有什么结果。
12.通常情况下U总是大大于m的,因此不可能完全避免冲突。冲突的频繁程度还与表的填满程度相关。装填因子α表示表中填入的结点数与表长的比值,通常取α≤1,因为α越大,表越满,冲突的机会也越大。
13.散列函数的选择有两条标准:简单和均匀。看看h(ki)=0这样的函数,简单是简单,但绝不均匀。
14.下面是常见的几种散列函数构的造方法:
(1)平方取中法
(2)除余法:它是用表长m来除关键字,取余数作为散列地址。若选除数m是关键字的基数的幂次,就会使得高位不同而低位相同的关键字互为同义词。因此最好选取素数为除数.
(3)相乘取整法:有两个步骤,先用关键字key乘上某个常数A(0)
(4)随机数法,此法以关键字为自变量,通过一随机函数得到的值作为散列地址。
15.处理冲突的方法:当不可避免发生冲突时,就必须对冲突加以解决,使发生冲突的同义词能存储到表中。
16.通常有两类方法处理冲突:开放定址法和拉链法。前者是将所有结点均存放在散列T[0..m-1]中,后者是将互为同义词的结点链成一个单链表,而将此链表的头指针放在散列表中。
17.开放定址法的一般形式为:hi=(h(key)+di)%m 1≤i≤m-1
18.开放定址法要求散列表的装填因子α≤1。开放定址法又有线性探查法、二次探查法和双重散列法之分。
(1)由于线性探查法在构造散列表时,遇到冲突(有同义词)的时候会按探查序列向后面的空地址插入,从而使原来应插入到此位置的结点又与它发生冲突,当一连串的位置均已有结点时,本应插入到这些位置的结点又只能将其插入到更后面的同一个空结点上,这种散列地址不同的结点争夺同一个后继散列地址的现象就是聚集或堆积。(注意,同义词发生冲突不是堆积)
为了减小堆积现象的发生,可以用二次探查法和双重散列法进行探查。
(2)拉链法解决冲突的做法是,将所有关键字为同义词的结点链接在同一个单链表中。
19.与开放定址法相比,拉链法有如下几个优点:
(1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;(简单无堆积)
(2)由于拉链法中各链表上的结点空间是动态申请的,故它更适于造表前无法确定表长的情况;(动态申表长)
(3)开放定址法为减少冲突要求装填因子α较小,当结点规模较大时会浪费很多空间,拉链法中α可以大于1,且结点较大时,其指针域可忽略不计,因此节省空间;(空间可节省)
(4)拉链法构造的散列表删除结点易实现,而开放定址法中则不能真正删除结点只能做删除标记。(删除易实现)
20.拉链法也有缺点:当结点规模较小时,用拉链法中的指针域也要占用额外空间,还是开放定址法省空间。
21.在散列表上的运算有查找、插入和删除,主要是查找。这三个操作的算法并不复杂,也容易理解。关于查找操作的时间性能,可看教材p202的表9.1。由表可见,散列表的平均查找长度不是结点个数n的函数,而是装填因子α的函数。α越小,冲突的概率越小,但空间的浪费将增加,当α大小合适时,散列表上的平均查找长度就是一个常数,时间性能是O(1).
第 十 章 文 件
1. 对数据结构来说,文件是性质相同的记录的集合。
2.
2.记录是文件中存取的基本单位,数据项是文件可使用的最小单位,数据项有时称字段或者属性。主关键字项(唯一标识一个记录的字段)、次关键字项、主关键字、次关键字。单关键字文件、多关键字文件等。
3.文件的逻辑结构是一种线性结构。
4.文件上的操作主要有两类:检索和维护。并有实时和批量处理两种处理方式。
5.文件的存储结构是指文件在外存上的组织方式,基本的组织方式有:顺序组织、索引组织、散列组织和链组织。文件组织的各种方式往往是这四种基本方式的结合。
6.常用的文件组织方式:顺序文件、索引文件、散列文件和多关键字文件。
7.评价一个文件组织的效率,是执行文件操作所花费的时间和文件组织所需的存储空间。通常文件组织的主要目的,是为了能高效、方便地对文件进行操作,而检索功能的多寡和速度的快慢,是衡量文件操作质量的重要标志。
8.顺序文件:是指按记录进入文件的先后顺序存放、其逻辑顺序和物理顺序一致的文件。
1)一切存储在顺序存储器(如磁带)上的文件都只能顺序文件。这种顺序文件只能按顺序查找法存取(注意,没有折半法了)
2)存储在直接存取存储器(如磁盘)上的顺序文件可以顺序查找法存取,也可以用分块查找法或二分查找法存取。
3)顺序文件多用于磁带。
9.索引文件:组织方式:通常是在文件本身(主文件)之外,另外建立一张表,它指明逻辑记录和物理记录之间一一对应的关系,这张表就叫做索引表,它和主文件一起构成索引文件。
1)索引非顺序文件中的索引表为稠密索引。索引顺序文件中的索引表为稀疏索引。
2)若记录很大使得索引表也很大时,可对索引表再建立索引,称为查找表。通常可达四级索引。
10.索引顺序文件:是最常用的文件组织:因为索引顺序文件的主文件也是有序的,所以它既适合于随机存取也适合于顺序存取。另一方面,索引非顺序文件的索引是稠密索引,而索引顺序文件的稀疏索引,占用空间较少,因此索引顺序文件是最常用的一种文件组织。
1)索引顺序文件常用的有两种:ISAM文件和VSAM文件
11.散列文件:是利用散列存储方式组织的文件,亦称为直接存取文件。
1)它类似于散列表,即根据文件中关键字的特点,设计一个散列函数和处理冲突的方法,将记录散列到存储设备上。与散列表不同的是,对于文件来说,记录通常是成组存放的,若干个记录组成一个存储单位,称为桶。对散列而言,处理冲突的方法主要采用拉链法。
2)散列文件的优点是:文件随机存放,记录不需要排序;插入删除方便;存取速度快;不需要索引区,节省存储空间。缺点是:不能进行顺序存取,只能按关键字随机存取,且询问方式限地简单询问,需要重新组织文件。
12.对被查询的次关键字也建立相应的索引,则这种包含有多个次关键字索引的文件称为多关键字文件。
1)两种多关键字文件的组织方法:多重表文件和倒排表。
2)一般的文件组织中,是先找记录,然后再找到该记录所含的各次关键字;而倒排文件是先给定次关键字,然后查找含有该次关键字的各个记录,因此称为倒排。
转自:http://blog.csdn.net/nixun/archive/2005/10/06/495716.aspx
第一天 时间:9/11/2003
真想不到,第一次上课竟然会是"9.11"事件纪念日.美国竟然还是不改老毛病,伊拉克战争死了多少平民百姓啊?!!!在此请先为死难者默哀3分钟,老美如果再这样多管闲事下去,上帝会二度惩罚美国人的啊!
能听到周SIR讲课真的挺开心的,我觉得他讲课比某些高程(高级工程师)还深动[转载时注:此处应该是 生动](当然,他的数据结构水平应该不亚于高程),为了考中程,上学期的<算法分析>选修课我也去揍了揍热闹.可惜由于SARS的关系,课只上了将近一半就停了.可以说我报程序员的原因就是因为有周SIR开导我们,听他的课真的是一种享受,不像大学里的某些人,水平不怎么高还在这里作威作福.
好了,发了这么多劳骚,开始转入正题了.
读这文章的人想必都是想报考或者将考程序员的同志们吧!首先一点必须问问自己,我考程序员的目的到底是为了什么?如果你的答案是:"只是为了拿一本证书".那我劝你早点GIVE UP吧!那样学起来会很累,因为没有兴趣.如果你想加入程序员的行列,为将来开发打下坚实的基础,那就试着看完这一小篇读书笔记吧!或许会对你有所帮助.
有句话必须记住:程序员考试仅仅是为了检验自己学到的而已,仅此而已!我想这便是程序员考试的最终意义所在吧!有些事情更注重过程!
数据结构
知识:
1.数据结构中对象的定义,存储的表示及操作的实现.
2.线性:线性表、栈、队列、数组、字符串(广义表不考)
树:二叉树
集合:查找,排序
图(不考)
能力:
分析,解决问题的能力
过程:
● 确定问题的数据。
● 确定数据间的关系。
● 确定存储结构(顺序-数组、链表-指针)
● 确定算法
● 编程
● 算法评价(时间和空间复杂度,主要考时间复杂度)
一、数组
1、存放于一个连续的空间
2、一维~多维数组的地址计算方式
已知data[0][0]的内存地址,且已知一个元素所占内存空间S求data[i][j]在内存中的地址。
公式:(add+(i*12+j)*S)(假设此数组为data[10][12])
注意:起始地址不是data[0][0]时候的情况。起始地址为data[-3][8]和情况;
3、顺序表的定义
存储表示及相关操作
4、顺序表操作中时间复杂度估计
5、字符串的定义(字符串就是线性表),存储表示
模式匹配算法(简单和KMP(不考))
6、特殊矩阵:存储方法(压缩存储(按行,按列))
三对角:存储于一维数组
三对角问题:已知Aij能求出在一维数组中的下标k;已知下标k求Aij。
7、稀疏矩阵:定义,存储方式:三元组表、十字链表(属于图部分,不考)
算法
● 数组中元素的原地逆置; 对换
● 在顺序表中搜索值为X的元素;
● 在有序表中搜索值为X的元素;(折半查找)
● 在顺序表中的第i个位置插入元素X;
● 在顺序表中的第i个位置删除元素X;
● 两个有序表的合并;算法?
线性表数据结构定义:
Typedef struct {
int data[max_size];
int len;
}linear_list;
● 模式匹配
● 字符串相加
● 求子串
● (i,j)<=>K 注意:不同矩阵所用的公式不同;
● 稀疏矩阵的转置(两种方式,后种为妙)
● 和数组有关的算法
例程:求两个长整数之和。
a=13056952168
b=87081299
数组:
a[]:1 3 0 5 6 9 5 2 1 6 8
b[]:8 7 0 8 1 2 9 9
由于以上的结构不够直观(一般越是直观越容易解决) 将其改为:
a[]:11 8 6 1 2 5 9 6 5 0 3 1 a[0]=11(位数)
b[]: 8 9 9 2 1 8 0 7 8 0 0 0 b[0]=8
c进位 0 1 1 0 0 1 1 1 1 0 0
c[]:11 7 6 4 3 3 0 4 4 2 3 1 c[0]的值(C位数)由c[max_s+1]决定!
注意:在求C前应该将C(max_s+1)位赋0.否则为随机数; 较小的整数高位赋0.
算法:已知a,b两个长整数,结果:c=a+b;
总共相加次数: max_s=max(a[],b[])
程序:
for(i=1;i<=max_s;i++) {
k=a[i]+b[i]+c[i];
c[i]=k%10;
c[i+1]=k/10;
}
求c位数:
if(c[max_s+1]==0)
c[0]=max_s;
else
c[0]=max_s+1;
以下代码是我编的(毕竟是初学者.不太简洁大家不要见怪!):
/*两长整数相加*/
#include<stdio.h>
#include<string.h>
#define PRIN printf("\n");
int flag=0; /*a[0]>b[0]?1:0*/
/* max(a[],b[]) {}*/
change(char da[],char db[],int a[],int b[],int c[]) {
int i;
if(a[0]>b[0]) {
for(i=1;i<=a[0];a[i]=da[a[0]-i]-'0',i++); /*a[0]-'0' so good!*/
for(i=1;i<=b[0];b[i]=db[b[0]-i]-'0',i++);
for(i=b[0]+1;i<=a[0];b[i]=0,i++);
for(i=1;i<=a[0]+1;c[i]=0,i++);
flag=1;
}
else {
for(i=1;i<=b[0];b[i]=db[b[0]-i]-'0',i++);
for(i=1;i<=a[0];a[i]=da[a[0]-i]-'0',i++);
for(i=a[0]+1;i<=b[0];a[i]=0,i++);
for(i=1;i<=b[0]+1;c[i]=0,i++);
}
}
add(int a[],int b[],int c[]) {
int i,sum;
if(flag==1) {
for(i=1;i<=a[0];i++) {
sum=a[i]+b[i]+c[i];
c[i+1]=sum/10;
c[i]=sum%10;
}
if(c[a[0]+1]==0)
c[0]=a[0];
else
c[0]=a[0]+1;
}
else {
for(i=1;i<=b[0];i++) {
sum=a[i]+b[i]+c[i];
c[i+1]=sum/10;
c[i]=sum%10;
}
if(c[b[0]+1]==0)
c[0]=b[0];
else
c[0]=b[0]+1;
}
}
void print(int m[]) {
int i;
for(i=m[0];i>=1;i--)
printf("%d,",m[i]); PRIN
}
main(){
int s;
int a[20],b[20],c[20];
char da[]={"123456789"};
char db[]={"12344443"};
a[0]=strlen(da);
b[0]=strlen(db);
printf("a[0]=%d\t",a[0]);
printf("b[0]=%d",b[0]); PRIN
change(da,db,a,b,c);
printf("flag=%d\n",flag); PRIN
printf("-----------------\n");
if(flag==1) {
print(a); PRIN
s=abs(a[0]-b[0]);
printf("+");
for(s=s*2-1;s>0;s--)
printf(" ");
print(b); PRIN
}
else {
s=abs(a[0]-b[0]);
printf("+");
for(s=s*2-1;s>0;s--)
printf(" ");
print(a); PRIN
print(b); PRIN
}
add(a,b,c);
printf("-----------------\n");
print(c);
}
时间复杂度计算:
● 确定基本操作
● 计算基本操作次数
● 选择T(n)
● lim(F(n)/T(n))=c
● 0(T(n))为时间复杂度
上例子的时间复杂度为O(max_s);
二:链表
1、知识点
●逻辑次序与物理次序不一致存储方法;
●单链表的定义:术语(头结点、头指针等)
●注意带头结点的单链表与不带头结点的单链表区别。(程序员考试一般不考带头结点,因为稍难理解)
●插入、删除、遍历(p==NULL表明操作完成)等操作
● 循环链表:定义,存储表示,操作;
● 双向链表:定义,存储方法,操作;
单链表和循环链表区别在最后一个指针域值不同。
2、操作
●单链表:插入X,删除X,查找X,计算结点个数
●单链表的逆置(中程曾考)
head->NULL/p->a1/p->a2/p->a3/p……an/NULL 注:p代表指针;NULL/p代表头结点
=》 head->NULL/p->an/p->an-1/p->an-2/p……a1/NULL
●循环链表的操作:插入X,删除X,查找X,计算结点个数;
用p=head->next来判断一次计算结点个数完成;
程序段如下:
k=0;
do{
k++;
p=p->next;
}while(p!=head->next);
● 双向链表
●多项式相加
● 有序链表合并
例程:已知两个字符串S,T,求S和T的最长公子串;
1、逻辑结构:字符串
2、存储结构:数组
3、算法: 精化(精细化工)**老顽童注:此处“精细化工”说明好像不对!
s="abaabcacb"
t="abdcabcaabcda"
当循环到s.len-1时,有两种情况:s="abaabcacb"、s="abaabcacb"
s.len-2时,有三种情况:s="abaabcacb"、s="abaabcacb"、s="abaabcacb"
.
.
.
1 s.len种情况
程序思路:
tag=0 //没有找到
for(l=s.len;l>0&&!tag;l--) {
判断长度为l的s中的子串是否为t的子串;
若是:tag=1;
}
长度为l的s的子串在s中有(s.len-l+1)个。
子串0: 0~l-1
1: 1~l
2: 2~l+1
3: 3~l+2
……
……
s.len-l: s.len-l~s.len-1
由上面可得:第j个子串为j~l+j-1。
判断长度为l的s中的子串是否为t的子串:
for(j=0;j<s.len-l+1&&!tag;j++){
判断s中长度为l的第j个子串是否为t的子串;
如果是:tag=1;
}
模式结构:
tag=0;
for(l=s.len;l>0&&tag==0;l--) {
for(j=0;j<s.len-l+1&&!tag;j++) {
?? 用模式匹配方法确定s[j]~s[l+j-1]这个字符串是否为t的子串; //好好想想
若是,tag=1;
}
}
第二天 时间:9/18/2003
转眼又过了一周了,前面一周里面我编了一些程序:链表,长整型数相加,三元组表转置以及一些简单的函数.其实有些算法想想是很简单,不过写起来还是需要一定耐心和C基础的,如果你自己觉得各算法都很懂了,不妨开机编编试试.或许会有一些新的发现与体会.
栈和队列
1、知识点:
● 栈的定义:操作受限的线性表
● 特点:后进先出
● 栈的存储结构:顺序,链接
/ push(s,d)
● 栈的基本操作:
\ pop(s)
栈定义:
struct {
datatype data[max_num];
int top;
};
●队列定义
特点:先进先出
/入队列 in_queue(Q,x)
●队列的操作:
\出队列 del_queue(Q)
●队列存储结构:
链队列:
Typedef struct node{
Datatype data;
Struct node *next;
}NODE;
Typedef struct {
NODE *front;
NODE *rear;
}Queue;
顺序队列:
struct {
datatype data[max_num];
int front,rear;
};
问题:
队列ó线性表
假溢出<=循環队列
队列满,队列空条件一样<=浪费一个存储空间
递归
定义:问题规模为N的解依赖于小规模问题的解。问题的求解通过小规模问题的解得到。
包括二个步骤:
1) 递推 6!=>5!=>4!=>3!=>2!=>1!=>0!
2) 回归 720<=120<=24<=6 <=2 <=1 <=0
递归工作栈实现递归的机制。
2、有关算法:
1) 顺序,链表结构下的出栈,入栈
2) 循環,队列的入队列,出队列。
3) 链队列的入队列,出队列。
4) 表达式计算:后缀表达式 35+6/4368/+*-
中缀表达式 (3+5)/6-4*(3+6/8)
由于中缀比较难处理,计算机内一般先将中缀转换为后缀。
运算:碰到操作数,不运算,碰到操符,运算其前两个操作数。
中缀=>后缀
5) 迷宫问题
6) 线性链表的递归算法 一个链表=一个结点+一个链表
int fuction(NODE *p) {
if(p==NULL) return 0;
else return(function(p->next));
}
树与二叉树
一、 知识点:
1. 树的定义: data_struct(D,R);
其中:D中有一个根,把D和出度去掉,可以分成M个部分.
D1,D2,D3,D4,D5…DM
R1,R2,R3,R4,R5…RM
而子树Ri形成树.
1) 递归定义 高度
2) 结点个数=1 |
此树的高度为2 |
2.二叉树定义:
结点个数>=0 .
3. 术语:左右孩子,双亲,子树,度,高度等概念.
4. 二叉树的性质
●层次为I的二叉树 I层结点 2I 个
●高度为H的二叉树结点 2H+1-1个
●H(点)=E(边)+1
●个数为N的完全二叉树高度为|_LOG2n_|
●完全二叉树结点编号:从上到下,从左到右.
i结点的双亲: | |_i/2_| | |_i-1/2_| | 1 | ||||||
i结点的左孩子: | 2i | 2i+1 | 2 | 3 | |||||
i结点的右孩子: | 2i+1 | 2i+2 | 4 | 5 | 6 | 7 | |||
(根) | 1为起点 | 0为起点 |
二叉树的存储结构:
1) 扩展成为完全二叉树,以一维数组存储。
A | |||||||||
B | C | ||||||||
D | E | F | |||||||
G | H | I |
数组下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
元素 | A | B | C | D | E | F | G | H | I |
2) 双亲表示法
数组下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
元素 | A | B | C | D | E | F | G | H | I |
双亲 | -1 | 0 | 0 | 1 | 2 | 2 | 3 | 3 | 4 |
3) 双亲孩子表示法
数组下标 | 0 | 1 | 2 | 3 | 4 | 5 | … |
元素 | A | B | C | D | E | F | … |
双亲 | -1 | 0 | 0 | 1 | 2 | 2 | … |
左子 | 1 | 3 | 4 | … | |||
右子 | 2 | -1 | 5 | … |
结构:
typedef struct {
datatype data;
int parent;
int lchild;
int rchild;
}NODE;
NODE tree[N]; // 生成N个结点的树
4) 二叉链表
5) 三叉链表
6) 哈夫曼树
5.二叉树的遍历
先根 \
中根 栈 中根遍历(左子树)根(右子树),再用相同的方法处理左子树,右子树.
后根 /
先,中序已知求树:先序找根,中序找确定左右子树.
层次遍历(队列实现)
6.线索二叉树(穿线树)
中序线索二树树目的:利用空指针直接得到中序遍历的结果.
手段(方法):左指针为空,指向前趋,右指针为空,指向后继.
结点结构:
ltag | Lch | Data | rch | rtag |
Ltag=0,lch指向左孩子,ltag=1,指向前趋结点
Rtag=0,rch指向右孩子;rtag=1,指向后继结点
中序遍历: 1) 找最左结点(其左指针为空)
2) 当该结点的rtag=1,该结点的rch指向的就为后继
3) 当rtag=0,后继元素为右子树中最左边那个
N个结点的二树有空指针N+1个
周六我去了周SIR的办公室,他很热情,我问的是中序线索化二叉树的问题(递归),关于这个问题我会在以后的笔记中作重点补充。我在这学校从来没有碰到过像他这样热情的老师,真的,大一的时候我们学校就开了C,当时我就连#include<stdio.h>这句话的意思都不晓得,别说是让我写程序了(到这份上也不怕把丑事都抖出来了:《数据结构》的课程设计也是哈科大的littlebob兄帮我做的,很遗憾,他高程就差几分,希望他早日成功,我们都要向他学习)等于说我的C知识九成都是在大二下学期的时候学的。而且全是自学的。拿这个周末来说吧。我三天时间就看完了一本C语言大全。当然,并不是从头到尾,只是根据自己的实际情况,重点是指针和数据结构那块。C最要的便是指针。程序员考试下午试题最重要的便是递归问题(1~2道,没有掌握就没希望了哦)。我说这些并不是为了表明自己多么用功,只是希望每位"学者"都有所侧重。
第三天 时间:9/23/2003
排序查找是我自己觉得最头疼的算法了,常搞混去的啊.不知道各位学得如何,如果不错,还请告诉我一些经验!
查找
一、 知识点 /静态查找->数组
1、 什么是查找
\动态查找->链树
●顺序查找,时间复杂度 O(n)
●折半查找:条件:有序;时间复杂度 O(nlog2n) (时间复杂度实际上是查找树的高度)
●索引查找:条件:第I+1块的所有元素都大于第I块的所有元素。
算法:根据index来确定X所在的块(i) 时间复杂度:m/2
在第I块里顺序查找X 时间复杂度:n/2
总的时间复杂度:(m+n)/2
●二叉排序树 1)定义:左子树键值大于根节点键值;右子树键值小于根的键值,其左右子树均为二叉排序树。
2)特点:中序遍历有序->(删除节点用到此性质)
3)二叉排序树的查找:如果根大于要查找的树,则前左子树前进,如果根小于要查找的树,则向右子树前进。
4)结点的插入->二叉排序树的构造方法
5)结点删除(难点) 1、右子树放在左子树的最右边
2、左子树放在右子树的最左边
●avl树(二叉平衡树):左右子树高度只能差1层,即|h|<=1其子树也一样。
●B树:n阶B树满足以下条件 1)每个结点(除根外)包含有N~2N个关链字。 2)所有叶子节点都在同一层。
3)B树的所有子树也是一棵B树。
特点:降低层次数,减少比较次数。
排序
一、知识点
1、排序的定义
/内排序:只在内存中进行
2、排序的分类
\外排序:与内外存进行排序
内排序: /直接插入排序
1)插入排序
\shell排序
/冒泡排序
2)交换排序
\快速排序
/简单选择排序
3)选择排序 堆
\ 锦标赛排序
4)归并排序(二路)
5)基数排序(多关链字排序)
3、时间复杂度(上午题目常考,不会求也得记住啊兄弟姐妹们!)
* * * * * * 15 * * * 15 * * *
/稳定 * * * * * * * * 15 15 * * * *(前后不变)
排序
\ 不稳定 * * * * * * * * 15 15 * * * *(前后改变)
经整理得:选择、希尔、堆、快速排序是不稳定的;直接插入、冒泡、合并排序是稳定的。
●锦标赛排序方法: 13 16 11 18 21 3 17 6
\ / \ / \ / \ /
13 11 3 6
\ / \ /
11 3
\ /
3(胜出,将其拿出,并令其为正无穷&Go On)
●归并排序方法: 13 16 11 18 21 3 17 6
\ / \ / \ / \ /
13,16 11,18 3,21 6,17
\ / \ /
11,13,16,18 3,6,17,21
\ /
3,6,11,13,16,17,18,21
●shell排序算法:1)定义一个步长(或者说增量)数组D[m];其中:D[m-1]=1(最后一个增量必须为1,否则可能不完全)
2)共排m趟,其中第i趟增量为D[i],把整个序列分成D[i]个子序列,分别对这D[i]个子序列进行直接插入排序。
程序如下: for(i=0;i<m;i++)
{for(j=0;j<d[i];j++)
{对第i个子序列进行直接插入排序;
注意:下标之差为D[i];
}
}
●快速排序 ( smaller )data ( bigger )
d[] i-> 13 16 11 18 21 3 17 6 24 8 <-j
先从后往前找,再从前往后找。
思想:空一个插入一个,i空j挪,j空i挪(这里的i,j是指i,j两指针所指的下标)。
一次执行算法:1)令temp=d[0](枢纽),i=0,j=n-1;
2)奇数次时从j位置出发向前找第一个比temp小的元素,找到后放到i的位置(d[i]=d[j];i++;) i往后挪。
3)偶数次时从i开始往后找第一个比temp大的数,(d[j]=d[i];j--;)
4)当i=j时,结束循环。d[i]=temp;
再用递归对左右进行快速排序,因为快速排序是一个典型的递归算法。
●堆排序
定义:d[n]满足条件:d[i]<d[2i+1]&&d[i]<d[2i+2] 大堆(上大下小)
d[i]>d[2i+1]&&d[i]>d[2i+2] 小堆(上小下大)
判断是否为堆应该将其转换成树的形式。总共排序n-1次
调整(重点)
程序: flag=0;
while(i<=n-1) {
if(d[i]<d[2*i+1])||(d[i]<d[2*i+2]))
{ if(d[2*i+1]>d[2*i+2]) 8 24 {d[i]<->d[2*i+1]; 24 21 -> 8 21
i=2*i+1;
else {
d[i]<->d[2*i+2];
i=2*i+2;
}
}
else
flag=1; //是堆
}
堆排序过程:
●基数排序(多关键字排序)
扑克: 1) 大小->分配
2) 花色->分配,收集
思想:分配再收集.
构建链表:链表个数根据关键字取值个数有关.
例:将下面九个三位数排序:
321 214 665 102 874 699 210 333 600
定义一个有十个元素的数组:
a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]
第一趟(个位): 210 321 102 333 214 665 699
600 874
结果: 210 600 321 102 333 214 874 665 699
第二趟(十位): 600 210 321 333 665 874 699
102 214
结果: 600 102 210 214 321 333 665 874 699
第三趟(百位): 102 210 321 600 874
214 333 665
699
结果: 102 210 214 321 333 600 665 699 874(排序成功)
最近在看一位程序员的笔记,也挺不错的啊.这应当是他的网站.他总说他的网站人气不够,现在顺便就帮他宣传一下啦!http://zhgpa.vicp.net/bbs,大家有时间多去去哦,呵呵!谢谢大伙支持!另外,还向大家推荐一个网站:http://kaowang.com/,挺不错的一个考试网站。学到不少东东啊!
八大类算法
程序员考试下午试题最后一道一般是八大类算法里头的.大家尤其要注意的是递归,因为近几年都考了,而且有的还考两题。可以说如果我们不掌握递归就没有掌握C,况且递归是C里的难点。为了控制合格率,程序员考试不会让我们轻松过关的,为了中国软件业,我想也应该这样啊。
/数据结构(离散)
迭代
\数值计算(连续)
枚举 策略好坏很重要
递推
递归
回溯
分治
贪婪
动态规划
其中:递推、递归、分治、动态规划四种算法思想基本相似。都是把大问题变成小问题,但技术上有差别。
第四天 时间:9/26/2003
枚举:
背包问题:
枚举策略:1)可能的方案:2N
2)对每一方案进行判断.
枚举法一般流程:
while(还有其他可能方案)
{ 按某种顺序可难方案;
检验方案;
if(方案为解)
保存方案;
}
}
枚举策略:
例:把所有排列枚举出来 P6=6!.
Min:123456
Max:654321
a1a2a3a4a5a6=>?(下一排列)=>?
比如:312654的下和种情况=>314256
递归
递归算法通常具有这样的特征:为求解规模为N的问题,设法将它分解成一些规模较小的问题,然后从这些较小问题的解能方便地构造出题目所需的解。而这些规模较小的问题也采用同样的方法分解成规模更小的问题,通过规模更小的问题构造出规模校小的问题的解,如此不断的反复分解和综合,总能分解到最简单的能直接得到解的情况。
因此,在解递归算法的题目时,要注意以下几点:
1) 找到递归调用的结束条件或继续递归调用条件.
2) 想方设法将处理对象的规模缩小或元素减少.
3) 由于递归调用可理解为并列同名函数的多次调用,而函数调用的原则是一层一层调用,一层一层返回.因此,还要注意理解调用返回后的下一个语句的作用.在一些简单的递归算法中,往往不需要考虑递调用返回后的语句处理.而在一些复杂的递归算法中,则需要考虑递归调用返回后的语句处理和进一步的递归调用.
4) 在读递归程序或编写递归程序时,必须要牢记递归函数的作用,这样便于理解整个函数的功能和知道哪儿需要写上递归调用语句.当然,在解递归算法的题目时,也需要分清递归函数中的内部变量和外部变量.
表现形式:
●定义是递归的(二叉树,二叉排序树)
●存储结构是递归的(二叉树,链表,数组)
●由前两种形式得出的算法是递归的
一般流程: function(variable list(规模为N))
{ if(规模小,解已知) return 解;
else {
把问题分成若干个部分;
某些部分可直接得到解;
而另一部分(规模为N-1)的解递归得到;
}
}
例1:求一个链表里的最大元素.
大家有没想过这个问题用递归来做呢?
非递归方法大家应该都会哦?
Max(nodetype *h) {
nodetype *p;
nodetype *q; //存放含最大值的结点
Int max=0;
P=h;
While(p!=NULL){
if (max<p->data) {
max=p->data;
q=p;
}
p=p->next;
}
return q;
}
下面真经来了,嘻嘻嘻~~~
*max(nodetype *h) {
nodetype *temp;
temp=max(h->next);
if(h->data>temp->data)
return h;
else
return temp;
}
大家有空想想下面这个算法:求链表所有数据的平均值(我也没试过),不许偷懒,用递归试试哦!
递归程序员考试题目类型:1)就是链表的某些操作(比如上面的求平均值)
2)二叉树(遍历等)
例2.判断数组元素是否递增
int jidge(int a[],int n) {
if(n==1) return 1;
else
if(a[0]>a[1]) return 0;
else return jidge(a+1,n-1);
}
例3.求二叉树的高度(根据二叉树的递归性质:(左子树)根(右子树))
int depth(nodetype *root) {
if(root==NULL)
return 0;
else {
h1=depth(root->lch);
h2=depth(root->rch);
return max(h1,h2)+1;
}
}
自己想想求二叉树结点个数(与上例类似)
例4.已知中序遍历和后序遍历,求二叉树.
设一二叉树的:
中序 S:E D F B A G J H C I
^start1 ^j ^end1
后序 T:E F D B J H G I C A
^start2 ^end2
node *create(char *s,char *t, int start1,int start2,int end1,int end2)
{ if (start1>end1) return NULL; //回归条件
root=(node *)malloc(sizeof(node));
root->data=t[end2];
找到S中T[end2]的位置为 j
root->lch=create(S,T,s1,j-1,start1,j+start2-start1-1);
root->rch=create(S,T,j+1,end1,j+start2-start1,end2-1);
return root;
}
例5.组合问题
n 个数: (1,2,3,4,…n)求从中取r个数的所有组合.
设n=5,r=3;
递归思想:先固定一位 5 (从另四个数当中选二个)
5,4 (从另三个数当中选一个)
5,4,3 (从另二个数当中选零个)
即:n-2个数中取r-2个数的所有组合
…
程序:
void combire(int n,int r) {
for(k=n;k>=n+r-1;k--) {
a[r]=k;
if(r==0) 打印a数组(表示找到一个解);
else combire(n-1,r-1);
}
}
第五天 9/28/2003
回溯法:
回溯跟递归都是程序员考试里常出现的问题,大家必须掌握!
回溯法的有关概念:
1) 解答树:叶子结点可能是解,对结点进行后序遍历.
2) 搜索与回溯
五个数中任选三个的解答树(解肯定有三层,至叶子结点):
ROOT 虚根
/ / | \ \
1 2 3 4 5
/ | | \ / | \ /\ |
2 3 4 5 3 4 5 4 5 5
/|\ /\ | /\ | |
3 4 5 4 5 5 4 5 5 5
回溯算法实现中的技巧:栈
要搞清回溯算法,先举一个(中序遍历二叉树的非递归算法)来说明栈在非递归中所起的作用。
A 过程:push()->push()->push()->push()栈内结果:ABDE(E为叶子,结束进栈)
/ \ pop() ABD(E无右孩子,出栈)
B C pop() AB(D无右孩子,出栈)
/\ pop() A(B有右孩子,右孩子进栈)
D F . .
/ /\ . .
E G H . .
/ . .
I 最后结果: EDBGFIHAC
简单算法:
…
if(r!=NULL) //树不空
{ while(r!=NULL)
{ push(s,r);
r=r->lch; //一直向左孩子前进
}
while(!empty(s)) // 栈非空,出栈
{ p=pop(s);
printf(p->data);
p=p->rch; //向右孩子前进
while(p!=NULL)
{ push(s,p);
p=p->lch; //右孩子进栈
}
}
} //这就是传说中的回溯,嘻嘻……没吓着你吧
5选3问题算法:
思想: 进栈:搜索
出栈:回溯
边建树(进栈)边遍历(出栈)
基本流程:
太复杂了,再说我不太喜欢用WORD画图(有损形象),以后再整理!
程序: n=5;r=3
……
init(s) //初始化栈
push(s,1) //根进栈
while(s.top<r-1)&&(s.data[s.top]!=n) //有孩子
push(s,s.data[s.top]+1); //孩子入栈
while(!empty(s))
{ if(s.top=r-1)
判断该"解"是否为解.
x=pop(s); //保留x,判断是否为最大值n,如果是n,则出栈
while(x==n)
x=pop(s);
push(s,x+1);
while(s.top<r-1)&&(s.data[s.top]!=n)
push(s,s.data[s.top]+1);
}
背包问题: TW=20 , w[5]={6,10,7,5,8}
解的条件:1) 该解答树的叶子结点
2) 重量最大
解答树如下: ROOT
/ | | | \
6 10 7 5 8
/ | | \ / | \ / \ |
10 7 5 8 7 5 8 5 8 8
| | |
5 8 8
程序:
temp_w 表示栈中重量和
…
init(s); //初始化栈
i=0;
While(w[i]>TW)
i++;
If(i==n) Return -1; //无解
Else {
Push(s,i);
Temp_w=w[i];
i++;
while(i<n)&&(temp_w+w[i]<=TW)
{ push(s,i);
temp_w+=w[i];
i++;
}
max_w=0;
while(!empty(s))
{ if(max_w<temp_w)
max_w=temp_w;
x=pop(s);
temp_w-=w[x];
x++;
while(x<n)&&(temp_w+w[x]>TW)
x++;
while(x<n)
{ push(s,x);
temp_w=temp_w+w[x];
x++;
while(x<n)&&(temp_w+w[x]>TW)
x++;
}
}
请大家思考:四色地图问题,比如给中国地图涂色,有四种颜色,每个省选一种颜色,相邻的省不能取同样的颜色.不许偷懒,不能选人口不多于xxxxW的"大国"哦!如果真的有一天台湾独立了,下场就是这样了,一种颜色就打发了,不过台湾的程序员们赚到了,省事!呵呵。
贪婪法:
不求最优解,速度快(以精确度换速度)
例:哈夫曼树,最小生成树
装箱问题:
有n个物品,重量分别为w[n],要把这n个物品装入载重为TW的集装箱内,需要几个集装箱?
思想1:对n个物品排序
拿出第1个集装箱,从大到小判断能不能放。
2 …
3 …
. …
. …
思想2: 对n个物品排序
用物品的重量去判断是否需要一只新箱子,如果物品重量小于本箱子所剩的载重量,则装进去,反之则取一只新箱子。
程序:
count=1;qw[0]=TW;
for(i=0;i<n;i++)
{
k=0;
while(k<count)&&(w[i]>qw[k])
k++;
if(w[i]<=qw[k])
qw[k]=qw[k]-w[i];
code[i]=k; //第i个物品放在第k个箱子内
else
{count++; //取一个新箱子
qw[count-1]=TW-w[i];
code[i]=count-1;
}
}
用贪婪法解背包问题:
n个物品,重量:w[n] 价值v[i]
背包限重TW,设计一个取法使得总价值最大.
方法:
0 1 2 3 … n-1
w0 w1 w2 w3 … wn-1
v0 v1 v2 v3 … vn-1
v0/w0 … v(n-1)/w(n-1) 求出各个物品的"性价比"
先按性价比从高到低进行排序
已知:w[n],v[n],TW
程序:
…
for(I=1;I<n;I++)
d[i]=v[i]/w[i]; //求性价比
for(I=0;I<n;I++)
{ max=-1;
for(j=0;j<n;j++)
{ if(d[j]>max)
{ max=d[j];x=j; }
}
e[i]=x;
d[x]=0;
}
temp_w=0;temp_v=0;
for(i=0;i<n;i++)
{ if(temp_w+w[e[i]]<=TW)
temp_v=temp_v+v[e[v]];
}
分治法:
思想:把规模为n的问题进行分解,分解成几个小规模的问题.然后在得到小规模问题的解的基础上,通过某种方法组合成该问题的解.
例:数轴上有n个点x[n],求距离最小的两个点.
分:任取一点,可以把x[i]这n个点分成两个部分
小的部分 分点 大的部分
|_._.__.__.____._|__._._.__._.__._______._.__._._.__.___._____._|
治:解=min{小的部分的距离最小值;
大的部分的距离最小值;
大的部分最小点和小的部分最大点这两点之差;}
第六天(想起了儿时的《最后一课》^_^) 10/7/2003
快考试了,老师没有多说什么,他只是给我们讲了讲近三年试题情况,并详细讲述了两道题目:一道递归,另一道回溯。不过今天不知怎么回事,我感觉特别累,也特别想睡觉。所以没什么感觉。这里就不说了,两道题目都有的,递归那题是2001年最后一题,回溯是在《程序员教程》P416,老师说高程曾经考过,有兴趣自己看看也能看懂的。老师特意为我们出了一份下午试题,我现在把他拿出来让大家参考参考。到这里,就到这里!
程序员考试下午试题(模拟)
一、把一个字符串插入到另一个字符串的某个位置(指元素个数)之后
char *insert(char *s,char *t,int position)
{ int i;
char *target;
if(position>strlen(t)) printf("error");
else
{ for (i=0;i< (1) ;i++)
{ if (i<position)
target[i]=s[i];
else
{ if(i< (2) )
target[i]=t[i];
else (3) ;
}
}
}
return garget;
}
二、辗转相除法求两个正整数的最大公约数
int f(int a,int b)
{ if (a==b) (4) ;
else
{ if (a>b) return f(a-b,b);
else (5) ;
}
}
三、求一个链表的所有元素的平均值
typedef struct { int num;
float ave;
}Back;
typedef struct node{ float data;
struct node *next;
} Node;
Back *aveage(Node *head)
{ Back *p,*q;
p=(Back *)malloc(sizeof(Back));
if (head==NULL)
{ p->num=0;
p->ave=0; }
else
{ (6) ;
p->num=q->num+1;
(7) ; }
retuen p;
}
main()
{ Node *h; Back *p;
h=create(); /*建立以h为头指针的链表*/
if (h==NULL) printf("没有元素");
else { p=aveage(h);
printf("链表元素的均值为:%6f",p->ave);
}
}
四、希尔排序
已知待排序序列data[n];希尔排序的增量序列为d[m],其中d[]序列降序排列,且d[m-1]=1。其方法是对序列进行m趟排序,在第i趟排序中,按增量d[i]把整个序列分成d[i]个子序列,并按直接插入排序的方法对每个子序列进行排序。
希尔排序的程序为:
void shellsort(int *data,int *d,int n,int m)
{ int i,j;
for (i=0;i<m;i++)
for (j=0; (1) ;j++)
shell( (2) );
}
void shell(int *data,int d,int num,int n)
{ int i,j,k,temp;
for (i=1; (3) ;i++)
{ j=0;
temp=data[j+i*d];
while ((j<i)&&( (4) ))
j++;
for (k=j;k<i;k++)
data[k+1]=data[k];
(5) ;
(6) }
}
五、求树的宽度
所谓宽度是指在二叉树的各层上,具有结点数最多的那一层上的结点总数。本算法是按层次遍历二叉树,采用一个队列q,让根结点入队列,最后出队列,若有左右子树,则左右子树根结点入队列,如此反复,直到队列为空。
int Width(BinTree *T)
{ int front=-1,rear=-1; /* 队列初始化*/
int flag=0,count=0,p;/*p用于指向树中层的最右边的结点,flag记录层中结点数的最大值。*/
if(T!=Null)
{ rear++; (1) ; flag=1; p=rear;
}
while( (2) )
{ front++;
T=q[front];
if(T->lchild!=Null)
{ rear++; (3) ; count++; } //
if(T->rchild!=Null)
{ rear++; q[rear]=T->rchild; (4) ; }
if(front==p) /* 当前层已遍历完毕*/
{ if( (5) ) flag=count; count=0; //
p=rear; /* p指向下一层最右边的结点*/
}
}
return(flag);
}
六、区间覆盖
设在实数轴上有n个点(x0,x1,……,xn-2,xn-1),现在要求用长度为1的单位闭区间去覆盖这n个点,则需要多少个单位闭区间。
int cover(float x[ ], int num)
{ float start[num],end[num];
int i ,j ,flag, count=0;
for (i=0;i<num;i++)
{ flag=1;
for (j=0;j< (1) ;j++)
{ if ((start[j]>x[i])&&(end[j]-x[i]<=1)) (2) ;
else if ( (3) ) end[j]=x[i];
else if ((x[i]>start[j])&&(x[i]<end[j])) flag=0;
if (flag) break;
}
if ( (4) )
{ end[count]=x[i]; (5); count++; }
}
return count-1;
}
start[count]=x[i]
七、围棋中的提子
在围棋比赛中,某一方(假设为黑方)在棋盘的某个位置(i,j)下子后,有可能提取对方(白方的一串子)。以W[19][19]表示一个棋盘,若W[i][j]=0表示在位置(i,j)上没有子,W[i][j]=1表示该位置上的是黑子,W[i][j]=-1表示该位置上是白子。可以用回溯法实现提子算法。
下列程序是黑棋(tag=1)下在(i,j)位置后判断是否可以吃掉某些白子,这些确定可以提掉的白子以一个线性表表示。
问题相应的数据结构有:
#define length 19 /*棋盘大小*/
#define max_num 361 /*棋盘中点的数量*/
struct position { int row; int col;
}; /*棋子位置*/
struct killed { struct position data[max_num]; int num;
} *p; /*存储可以吃掉的棋子位置*/
struct stack { struct position node[max_num]; int top;
}; /*栈*/
int w[length][length]; /*棋盘中双方的棋子分布*/
int visited[length][length]; /*给已搜索到的棋子位置作标记,初值为0,搜索到后为1*/
struct killed *kill(int w[length][length],int r,int c,int tag)
{ struct killed *p;
struct position *s;
struct stack S;
for (i=0;i<length;i++)
for (j=0;j<length;j++)
(1) ;
S.top=-1; p->num=-1;
if (w[r-1][c]==tag*(-1)) s->row=r-1; s->col=c;
else if (w[r+1][c]==tag*(-1)) s->row=r+1; s->col=c;
else if (w[r][c-1]==tag*(-1)) s->row=r; s->col=c-1;
else if (w[r][c+1]==tag*(-1)) s->row=r; s->col=c+1;
else p->len=0; return p;
push(S,s); visited[s->row][s->col]=1;
flag=search(s,tag);
while ( (2))
{ push(S,s); visited[s->row][s->col]=1;
(3);
}
while (S->top>=0)
{ pop(S);
(4);
flag=search(s,tag);
while (flag)
{ push(S,s);
visit(s);
flag=search(s);
}
}
}
void push( struct stack *S, struct position *s)
{ S->top++;
S->node[S->top].row=s->row;
S->node[S->top].col=s->col;
p->num++;
p->data[p->num].row=s->row;
p->data[p->num].col=s->col;
}
void pop(struct stack *S)
{ S->top--;
}
struct position *gettop(struct stack *S)
{ struct position *s;
s->row=S->data[S->top].row;
s->row=S->data[S->top].row;
return s;
}
int search(struct position *s,int tag)
{ int row,col;
row=s->row; col=s->col;
if (W[row+1][col]=(-1)*tag)&&(!visited[row+1][col])
{ s->row=row+1;s->col=col; return 1;}
if (W[row-1][col]=(-1)*tag)&&(!visited[row-1][col])
{ s->row=row-1;s->col=col; return 1;}
if (W[row][col+1]=(-1)*tag)&&(!visited[row][col+1])
{ s->row=row;s->col=col+1; return 1;}
if (W[row][col-1]=(-1)*tag)&&(!visited[row][col-1])
{ s->row=row;s->col=col-1; return 1}
(5);
}
答案:
(1)strlen(s)+strlen(t) (2)position+strlen(t) (3)target[i]=s[i-strlen(t)]
(4)return a (5)return f(a,b-a)
(6)q=aveage(head->next) (7)p->ave=(head->data+q->ave*q->num)/p->num
(1)j<d[i] (2)data,d[i],j,n (3)num+i*d<n (4)data[j+i*d]<temp (5)data[j]=temp
(1)q[rear]=T (2)front<p (3)q[rear]=T->lchild (4)count++ (5)flag<count
(1)count (2)(x[i]>end[j])&&(x[i]-start[j]<=1) (3)start[j]=x[i] (4)!flag (5)
(1)visited[i][j]=0 (2)flag (3)flag=search(s,tag) (4)s=gettop(S) (5)return 0
课已全部授完,也该收笔了.望对大家有所启发吧!
最后祝大家顺利过关
2.二叉树定义:
结点个数>=0 .
3. 术语:左右孩子,双亲,子树,度,高度等概念.
4. 二叉树的性质
●层次为I的二叉树 I层结点 2I 个
●高度为H的二叉树结点 2H+1-1个
●H(点)=E(边)+1
●个数为N的完全二叉树高度为|_LOG2n_|
●完全二叉树结点编号:从上到下,从左到右.
i结点的双亲: | |_i/2_| | |_i-1/2_| | 1 | ||||||
i结点的左孩子: | 2i | 2i+1 | 2 | 3 | |||||
i结点的右孩子: | 2i+1 | 2i+2 | 4 | 5 | 6 | 7 | |||
(根) | 1为起点 | 0为起点 |
二叉树的存储结构:
1) 扩展成为完全二叉树,以一维数组存储。
A | |||||||||
B | C | ||||||||
D | E | F | |||||||
G | H | I |
数组下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
元素 | A | B | C | D | E | F | G | H | I |
2) 双亲表示法
数组下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
元素 | A | B | C | D | E | F | G | H | I |
双亲 | -1 | 0 | 0 | 1 | 2 | 2 | 3 | 3 | 4 |
3) 双亲孩子表示法
数组下标 | 0 | 1 | 2 | 3 | 4 | 5 | … |
元素 | A | B | C | D | E | F | … |
双亲 | -1 | 0 | 0 | 1 | 2 | 2 | … |
左子 | 1 | 3 | 4 | … | |||
右子 | 2 | -1 | 5 | … |
结构:
typedef struct {
datatype data;
int parent;
int lchild;
int rchild;
}NODE;
NODE tree[N]; // 生成N个结点的树
4) 二叉链表
5) 三叉链表
6) 哈夫曼树
5.二叉树的遍历
先根 \
中根 栈 中根遍历(左子树)根(右子树),再用相同的方法处理左子树,右子树.
后根 /
先,中序已知求树:先序找根,中序找确定左右子树.
层次遍历(队列实现)
6.线索二叉树(穿线树)
中序线索二树树目的:利用空指针直接得到中序遍历的结果.
手段(方法):左指针为空,指向前趋,右指针为空,指向后继.
结点结构:
ltag | Lch | Data | rch | rtag |
Ltag=0,lch指向左孩子,ltag=1,指向前趋结点
Rtag=0,rch指向右孩子;rtag=1,指向后继结点
中序遍历: 1) 找最左结点(其左指针为空)
2) 当该结点的rtag=1,该结点的rch指向的就为后继
3) 当rtag=0,后继元素为右子树中最左边那个
N个结点的二树有空指针N+1个
周六我去了周SIR的办公室,他很热情,我问的是中序线索化二叉树的问题(递归),关于这个问题我会在以后的笔记中作重点补充。我在这学校从来没有碰到过像他这样热情的老师,真的,大一的时候我们学校就开了C,当时我就连#include<stdio.h>这句话的意思都不晓得,别说是让我写程序了(到这份上也不怕把丑事都抖出来了:《数据结构》的课程设计也是哈科大的littlebob兄帮我做的,很遗憾,他高程就差几分,希望他早日成功,我们都要向他学习)等于说我的C知识九成都是在大二下学期的时候学的。而且全是自学的。拿这个周末来说吧。我三天时间就看完了一本C语言大全。当然,并不是从头到尾,只是根据自己的实际情况,重点是指针和数据结构那块。C最要的便是指针。程序员考试下午试题最重要的便是递归问题(1~2道,没有掌握就没希望了哦)。我说这些并不是为了表明自己多么用功,只是希望每位"学者"都有所侧重。
第三天 时间:9/23/2003
排序查找是我自己觉得最头疼的算法了,常搞混去的啊.不知道各位学得如何,如果不错,还请告诉我一些经验!
查找
一、 知识点 /静态查找->数组
1、 什么是查找
\动态查找->链树
●顺序查找,时间复杂度 O(n)
●折半查找:条件:有序;时间复杂度 O(nlog2n) (时间复杂度实际上是查找树的高度)
●索引查找:条件:第I+1块的所有元素都大于第I块的所有元素。
算法:根据index来确定X所在的块(i) 时间复杂度:m/2
在第I块里顺序查找X 时间复杂度:n/2
总的时间复杂度:(m+n)/2
●二叉排序树 1)定义:左子树键值大于根节点键值;右子树键值小于根的键值,其左右子树均为二叉排序树。
2)特点:中序遍历有序->(删除节点用到此性质)
3)二叉排序树的查找:如果根大于要查找的树,则前左子树前进,如果根小于要查找的树,则向右子树前进。
4)结点的插入->二叉排序树的构造方法
5)结点删除(难点) 1、右子树放在左子树的最右边
2、左子树放在右子树的最左边
●avl树(二叉平衡树):左右子树高度只能差1层,即|h|<=1其子树也一样。
●B树:n阶B树满足以下条件 1)每个结点(除根外)包含有N~2N个关链字。 2)所有叶子节点都在同一层。
3)B树的所有子树也是一棵B树。
特点:降低层次数,减少比较次数。
排序
一、知识点
1、排序的定义
/内排序:只在内存中进行
2、排序的分类
\外排序:与内外存进行排序
内排序: /直接插入排序
1)插入排序
\shell排序
/冒泡排序
2)交换排序
\快速排序
/简单选择排序
3)选择排序 堆
\ 锦标赛排序
4)归并排序(二路)
5)基数排序(多关链字排序)
3、时间复杂度(上午题目常考,不会求也得记住啊兄弟姐妹们!)
* * * * * * 15 * * * 15 * * *
/稳定 * * * * * * * * 15 15 * * * *(前后不变)
排序
\ 不稳定 * * * * * * * * 15 15 * * * *(前后改变)
经整理得:选择、希尔、堆、快速排序是不稳定的;直接插入、冒泡、合并排序是稳定的。
●锦标赛排序方法: 13 16 11 18 21 3 17 6
\ / \ / \ / \ /
13 11 3 6
\ / \ /
11 3
\ /
3(胜出,将其拿出,并令其为正无穷&Go On)
●归并排序方法: 13 16 11 18 21 3 17 6
\ / \ / \ / \ /
13,16 11,18 3,21 6,17
\ / \ /
11,13,16,18 3,6,17,21
\ /
3,6,11,13,16,17,18,21
●shell排序算法:1)定义一个步长(或者说增量)数组D[m];其中:D[m-1]=1(最后一个增量必须为1,否则可能不完全)
2)共排m趟,其中第i趟增量为D[i],把整个序列分成D[i]个子序列,分别对这D[i]个子序列进行直接插入排序。
程序如下: for(i=0;i<m;i++)
{for(j=0;j<d[i];j++)
{对第i个子序列进行直接插入排序;
注意:下标之差为D[i];
}
}
●快速排序 ( smaller )data ( bigger )
d[] i-> 13 16 11 18 21 3 17 6 24 8 <-j
先从后往前找,再从前往后找。
思想:空一个插入一个,i空j挪,j空i挪(这里的i,j是指i,j两指针所指的下标)。
一次执行算法:1)令temp=d[0](枢纽),i=0,j=n-1;
2)奇数次时从j位置出发向前找第一个比temp小的元素,找到后放到i的位置(d[i]=d[j];i++;) i往后挪。
3)偶数次时从i开始往后找第一个比temp大的数,(d[j]=d[i];j--;)
4)当i=j时,结束循环。d[i]=temp;
再用递归对左右进行快速排序,因为快速排序是一个典型的递归算法。
●堆排序
定义:d[n]满足条件:d[i]<d[2i+1]&&d[i]<d[2i+2] 大堆(上大下小)
d[i]>d[2i+1]&&d[i]>d[2i+2] 小堆(上小下大)
判断是否为堆应该将其转换成树的形式。总共排序n-1次
调整(重点)
程序: flag=0;
while(i<=n-1) {
if(d[i]<d[2*i+1])||(d[i]<d[2*i+2]))
{ if(d[2*i+1]>d[2*i+2]) 8 24 {d[i]<->d[2*i+1]; 24 21 -> 8 21
i=2*i+1;
else {
d[i]<->d[2*i+2];
i=2*i+2;
}
}
else
flag=1; //是堆
}
堆排序过程:
●基数排序(多关键字排序)
扑克: 1) 大小->分配
2) 花色->分配,收集
思想:分配再收集.
构建链表:链表个数根据关键字取值个数有关.
例:将下面九个三位数排序:
321 214 665 102 874 699 210 333 600
定义一个有十个元素的数组:
a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]
第一趟(个位): 210 321 102 333 214 665 699
600 874
结果: 210 600 321 102 333 214 874 665 699
第二趟(十位): 600 210 321 333 665 874 699
102 214
结果: 600 102 210 214 321 333 665 874 699
第三趟(百位): 102 210 321 600 874
214 333 665
699
结果: 102 210 214 321 333 600 665 699 874(排序成功)
最近在看一位程序员的笔记,也挺不错的啊.这应当是他的网站.他总说他的网站人气不够,现在顺便就帮他宣传一下啦!http://zhgpa.vicp.net/bbs,大家有时间多去去哦,呵呵!谢谢大伙支持!另外,还向大家推荐一个网站:http://kaowang.com/,挺不错的一个考试网站。学到不少东东啊!
八大类算法
程序员考试下午试题最后一道一般是八大类算法里头的.大家尤其要注意的是递归,因为近几年都考了,而且有的还考两题。可以说如果我们不掌握递归就没有掌握C,况且递归是C里的难点。为了控制合格率,程序员考试不会让我们轻松过关的,为了中国软件业,我想也应该这样啊。
/数据结构(离散)
迭代
\数值计算(连续)
枚举 策略好坏很重要
递推
递归
回溯
分治
贪婪
动态规划
其中:递推、递归、分治、动态规划四种算法思想基本相似。都是把大问题变成小问题,但技术上有差别。
第四天 时间:9/26/2003
枚举:
背包问题:
枚举策略:1)可能的方案:2N
2)对每一方案进行判断.
枚举法一般流程:
while(还有其他可能方案)
{ 按某种顺序可难方案;
检验方案;
if(方案为解)
保存方案;
}
}
枚举策略:
例:把所有排列枚举出来 P6=6!.
Min:123456
Max:654321
a1a2a3a4a5a6=>?(下一排列)=>?
比如:312654的下和种情况=>314256
递归
递归算法通常具有这样的特征:为求解规模为N的问题,设法将它分解成一些规模较小的问题,然后从这些较小问题的解能方便地构造出题目所需的解。而这些规模较小的问题也采用同样的方法分解成规模更小的问题,通过规模更小的问题构造出规模校小的问题的解,如此不断的反复分解和综合,总能分解到最简单的能直接得到解的情况。
因此,在解递归算法的题目时,要注意以下几点:
1) 找到递归调用的结束条件或继续递归调用条件.
2) 想方设法将处理对象的规模缩小或元素减少.
3) 由于递归调用可理解为并列同名函数的多次调用,而函数调用的原则是一层一层调用,一层一层返回.因此,还要注意理解调用返回后的下一个语句的作用.在一些简单的递归算法中,往往不需要考虑递调用返回后的语句处理.而在一些复杂的递归算法中,则需要考虑递归调用返回后的语句处理和进一步的递归调用.
4) 在读递归程序或编写递归程序时,必须要牢记递归函数的作用,这样便于理解整个函数的功能和知道哪儿需要写上递归调用语句.当然,在解递归算法的题目时,也需要分清递归函数中的内部变量和外部变量.
表现形式:
●定义是递归的(二叉树,二叉排序树)
●存储结构是递归的(二叉树,链表,数组)
●由前两种形式得出的算法是递归的
一般流程: function(variable list(规模为N))
{ if(规模小,解已知) return 解;
else {
把问题分成若干个部分;
某些部分可直接得到解;
而另一部分(规模为N-1)的解递归得到;
}
}
例1:求一个链表里的最大元素.
大家有没想过这个问题用递归来做呢?
非递归方法大家应该都会哦?
Max(nodetype *h) {
nodetype *p;
nodetype *q; //存放含最大值的结点
Int max=0;
P=h;
While(p!=NULL){
if (max<p->data) {
max=p->data;
q=p;
}
p=p->next;
}
return q;
}
下面真经来了,嘻嘻嘻~~~
*max(nodetype *h) {
nodetype *temp;
temp=max(h->next);
if(h->data>temp->data)
return h;
else
return temp;
}
大家有空想想下面这个算法:求链表所有数据的平均值(我也没试过),不许偷懒,用递归试试哦!
递归程序员考试题目类型:1)就是链表的某些操作(比如上面的求平均值)
2)二叉树(遍历等)
例2.判断数组元素是否递增
int jidge(int a[],int n) {
if(n==1) return 1;
else
if(a[0]>a[1]) return 0;
else return jidge(a+1,n-1);
}
例3.求二叉树的高度(根据二叉树的递归性质:(左子树)根(右子树))
int depth(nodetype *root) {
if(root==NULL)
return 0;
else {
h1=depth(root->lch);
h2=depth(root->rch);
return max(h1,h2)+1;
}
}
自己想想求二叉树结点个数(与上例类似)
例4.已知中序遍历和后序遍历,求二叉树.
设一二叉树的:
中序 S:E D F B A G J H C I
^start1 ^j ^end1
后序 T:E F D B J H G I C A
^start2 ^end2
node *create(char *s,char *t, int start1,int start2,int end1,int end2)
{ if (start1>end1) return NULL; //回归条件
root=(node *)malloc(sizeof(node));
root->data=t[end2];
找到S中T[end2]的位置为 j
root->lch=create(S,T,s1,j-1,start1,j+start2-start1-1);
root->rch=create(S,T,j+1,end1,j+start2-start1,end2-1);
return root;
}
例5.组合问题
n 个数: (1,2,3,4,…n)求从中取r个数的所有组合.
设n=5,r=3;
递归思想:先固定一位 5 (从另四个数当中选二个)
5,4 (从另三个数当中选一个)
5,4,3 (从另二个数当中选零个)
即:n-2个数中取r-2个数的所有组合
…
程序:
void combire(int n,int r) {
for(k=n;k>=n+r-1;k--) {
a[r]=k;
if(r==0) 打印a数组(表示找到一个解);
else combire(n-1,r-1);
}
}
第五天 9/28/2003
回溯法:
回溯跟递归都是程序员考试里常出现的问题,大家必须掌握!
回溯法的有关概念:
1) 解答树:叶子结点可能是解,对结点进行后序遍历.
2) 搜索与回溯
五个数中任选三个的解答树(解肯定有三层,至叶子结点):
ROOT 虚根
/ / | \ \
1 2 3 4 5
/ | | \ / | \ /\ |
2 3 4 5 3 4 5 4 5 5
/|\ /\ | /\ | |
3 4 5 4 5 5 4 5 5 5
回溯算法实现中的技巧:栈
要搞清回溯算法,先举一个(中序遍历二叉树的非递归算法)来说明栈在非递归中所起的作用。
A 过程:push()->push()->push()->push()栈内结果:ABDE(E为叶子,结束进栈)
/ \ pop() ABD(E无右孩子,出栈)
B C pop() AB(D无右孩子,出栈)
/\ pop() A(B有右孩子,右孩子进栈)
D F . .
/ /\ . .
E G H . .
/ . .
I 最后结果: EDBGFIHAC
简单算法:
…
if(r!=NULL) //树不空
{ while(r!=NULL)
{ push(s,r);
r=r->lch; //一直向左孩子前进
}
while(!empty(s)) // 栈非空,出栈
{ p=pop(s);
printf(p->data);
p=p->rch; //向右孩子前进
while(p!=NULL)
{ push(s,p);
p=p->lch; //右孩子进栈
}
}
} //这就是传说中的回溯,嘻嘻……没吓着你吧
5选3问题算法:
思想: 进栈:搜索
出栈:回溯
边建树(进栈)边遍历(出栈)
基本流程:
太复杂了,再说我不太喜欢用WORD画图(有损形象),以后再整理!
程序: n=5;r=3
……
init(s) //初始化栈
push(s,1) //根进栈
while(s.top<r-1)&&(s.data[s.top]!=n) //有孩子
push(s,s.data[s.top]+1); //孩子入栈
while(!empty(s))
{ if(s.top=r-1)
判断该"解"是否为解.
x=pop(s); //保留x,判断是否为最大值n,如果是n,则出栈
while(x==n)
x=pop(s);
push(s,x+1);
while(s.top<r-1)&&(s.data[s.top]!=n)
push(s,s.data[s.top]+1);
}
背包问题: TW=20 , w[5]={6,10,7,5,8}
解的条件:1) 该解答树的叶子结点
2) 重量最大
解答树如下: ROOT
/ | | | \
6 10 7 5 8
/ | | \ / | \ / \ |
10 7 5 8 7 5 8 5 8 8
| | |
5 8 8
程序:
temp_w 表示栈中重量和
…
init(s); //初始化栈
i=0;
While(w[i]>TW)
i++;
If(i==n) Return -1; //无解
Else {
Push(s,i);
Temp_w=w[i];
i++;
while(i<n)&&(temp_w+w[i]<=TW)
{ push(s,i);
temp_w+=w[i];
i++;
}
max_w=0;
while(!empty(s))
{ if(max_w<temp_w)
max_w=temp_w;
x=pop(s);
temp_w-=w[x];
x++;
while(x<n)&&(temp_w+w[x]>TW)
x++;
while(x<n)
{ push(s,x);
temp_w=temp_w+w[x];
x++;
while(x<n)&&(temp_w+w[x]>TW)
x++;
}
}
请大家思考:四色地图问题,比如给中国地图涂色,有四种颜色,每个省选一种颜色,相邻的省不能取同样的颜色.不许偷懒,不能选人口不多于xxxxW的"大国"哦!如果真的有一天台湾独立了,下场就是这样了,一种颜色就打发了,不过台湾的程序员们赚到了,省事!呵呵。
贪婪法:
不求最优解,速度快(以精确度换速度)
例:哈夫曼树,最小生成树
装箱问题:
有n个物品,重量分别为w[n],要把这n个物品装入载重为TW的集装箱内,需要几个集装箱?
思想1:对n个物品排序
拿出第1个集装箱,从大到小判断能不能放。
2 …
3 …
. …
. …
思想2: 对n个物品排序
用物品的重量去判断是否需要一只新箱子,如果物品重量小于本箱子所剩的载重量,则装进去,反之则取一只新箱子。
程序:
count=1;qw[0]=TW;
for(i=0;i<n;i++)
{
k=0;
while(k<count)&&(w[i]>qw[k])
k++;
if(w[i]<=qw[k])
qw[k]=qw[k]-w[i];
code[i]=k; //第i个物品放在第k个箱子内
else
{count++; //取一个新箱子
qw[count-1]=TW-w[i];
code[i]=count-1;
}
}
用贪婪法解背包问题:
n个物品,重量:w[n] 价值v[i]
背包限重TW,设计一个取法使得总价值最大.
方法:
0 1 2 3 … n-1
w0 w1 w2 w3 … wn-1
v0 v1 v2 v3 … vn-1
v0/w0 … v(n-1)/w(n-1) 求出各个物品的"性价比"
先按性价比从高到低进行排序
已知:w[n],v[n],TW
程序:
…
for(I=1;I<n;I++)
d[i]=v[i]/w[i]; //求性价比
for(I=0;I<n;I++)
{ max=-1;
for(j=0;j<n;j++)
{ if(d[j]>max)
{ max=d[j];x=j; }
}
e[i]=x;
d[x]=0;
}
temp_w=0;temp_v=0;
for(i=0;i<n;i++)
{ if(temp_w+w[e[i]]<=TW)
temp_v=temp_v+v[e[v]];
}
分治法:
思想:把规模为n的问题进行分解,分解成几个小规模的问题.然后在得到小规模问题的解的基础上,通过某种方法组合成该问题的解.
例:数轴上有n个点x[n],求距离最小的两个点.
分:任取一点,可以把x[i]这n个点分成两个部分
小的部分 分点 大的部分
|_._.__.__.____._|__._._.__._.__._______._.__._._.__.___._____._|
治:解=min{小的部分的距离最小值;
大的部分的距离最小值;
大的部分最小点和小的部分最大点这两点之差;}
第六天(想起了儿时的《最后一课》^_^) 10/7/2003
快考试了,老师没有多说什么,他只是给我们讲了讲近三年试题情况,并详细讲述了两道题目:一道递归,另一道回溯。不过今天不知怎么回事,我感觉特别累,也特别想睡觉。所以没什么感觉。这里就不说了,两道题目都有的,递归那题是2001年最后一题,回溯是在《程序员教程》P416,老师说高程曾经考过,有兴趣自己看看也能看懂的。老师特意为我们出了一份下午试题,我现在把他拿出来让大家参考参考。到这里,就到这里!
程序员考试下午试题(模拟)
一、把一个字符串插入到另一个字符串的某个位置(指元素个数)之后
char *insert(char *s,char *t,int position)
{ int i;
char *target;
if(position>strlen(t)) printf("error");
else
{ for (i=0;i< (1) ;i++)
{ if (i<position)
target[i]=s[i];
else
{ if(i< (2) )
target[i]=t[i];
else (3) ;
}
}
}
return garget;
}
二、辗转相除法求两个正整数的最大公约数
int f(int a,int b)
{ if (a==b) (4) ;
else
{ if (a>b) return f(a-b,b);
else (5) ;
}
}
三、求一个链表的所有元素的平均值
typedef struct { int num;
float ave;
}Back;
typedef struct node{ float data;
struct node *next;
} Node;
Back *aveage(Node *head)
{ Back *p,*q;
p=(Back *)malloc(sizeof(Back));
if (head==NULL)
{ p->num=0;
p->ave=0; }
else
{ (6) ;
p->num=q->num+1;
(7) ; }
retuen p;
}
main()
{ Node *h; Back *p;
h=create(); /*建立以h为头指针的链表*/
if (h==NULL) printf("没有元素");
else { p=aveage(h);
printf("链表元素的均值为:%6f",p->ave);
}
}
四、希尔排序
已知待排序序列data[n];希尔排序的增量序列为d[m],其中d[]序列降序排列,且d[m-1]=1。其方法是对序列进行m趟排序,在第i趟排序中,按增量d[i]把整个序列分成d[i]个子序列,并按直接插入排序的方法对每个子序列进行排序。
希尔排序的程序为:
void shellsort(int *data,int *d,int n,int m)
{ int i,j;
for (i=0;i<m;i++)
for (j=0; (1) ;j++)
shell( (2) );
}
void shell(int *data,int d,int num,int n)
{ int i,j,k,temp;
for (i=1; (3) ;i++)
{ j=0;
temp=data[j+i*d];
while ((j<i)&&( (4) ))
j++;
for (k=j;k<i;k++)
data[k+1]=data[k];
(5) ;
(6) }
}
五、求树的宽度
所谓宽度是指在二叉树的各层上,具有结点数最多的那一层上的结点总数。本算法是按层次遍历二叉树,采用一个队列q,让根结点入队列,最后出队列,若有左右子树,则左右子树根结点入队列,如此反复,直到队列为空。
int Width(BinTree *T)
{ int front=-1,rear=-1; /* 队列初始化*/
int flag=0,count=0,p;/*p用于指向树中层的最右边的结点,flag记录层中结点数的最大值。*/
if(T!=Null)
{ rear++; (1) ; flag=1; p=rear;
}
while( (2) )
{ front++;
T=q[front];
if(T->lchild!=Null)
{ rear++; (3) ; count++; } //
if(T->rchild!=Null)
{ rear++; q[rear]=T->rchild; (4) ; }
if(front==p) /* 当前层已遍历完毕*/
{ if( (5) ) flag=count; count=0; //
p=rear; /* p指向下一层最右边的结点*/
}
}
return(flag);
}
六、区间覆盖
设在实数轴上有n个点(x0,x1,……,xn-2,xn-1),现在要求用长度为1的单位闭区间去覆盖这n个点,则需要多少个单位闭区间。
int cover(float x[ ], int num)
{ float start[num],end[num];
int i ,j ,flag, count=0;
for (i=0;i<num;i++)
{ flag=1;
for (j=0;j< (1) ;j++)
{ if ((start[j]>x[i])&&(end[j]-x[i]<=1)) (2) ;
else if ( (3) ) end[j]=x[i];
else if ((x[i]>start[j])&&(x[i]<end[j])) flag=0;
if (flag) break;
}
if ( (4) )
{ end[count]=x[i]; (5); count++; }
}
return count-1;
}
start[count]=x[i]
七、围棋中的提子
在围棋比赛中,某一方(假设为黑方)在棋盘的某个位置(i,j)下子后,有可能提取对方(白方的一串子)。以W[19][19]表示一个棋盘,若W[i][j]=0表示在位置(i,j)上没有子,W[i][j]=1表示该位置上的是黑子,W[i][j]=-1表示该位置上是白子。可以用回溯法实现提子算法。
下列程序是黑棋(tag=1)下在(i,j)位置后判断是否可以吃掉某些白子,这些确定可以提掉的白子以一个线性表表示。
问题相应的数据结构有:
#define length 19 /*棋盘大小*/
#define max_num 361 /*棋盘中点的数量*/
struct position { int row; int col;
}; /*棋子位置*/
struct killed { struct position data[max_num]; int num;
} *p; /*存储可以吃掉的棋子位置*/
struct stack { struct position node[max_num]; int top;
}; /*栈*/
int w[length][length]; /*棋盘中双方的棋子分布*/
int visited[length][length]; /*给已搜索到的棋子位置作标记,初值为0,搜索到后为1*/
struct killed *kill(int w[length][length],int r,int c,int tag)
{ struct killed *p;
struct position *s;
struct stack S;
for (i=0;i<length;i++)
for (j=0;j<length;j++)
(1) ;
S.top=-1; p->num=-1;
if (w[r-1][c]==tag*(-1)) s->row=r-1; s->col=c;
else if (w[r+1][c]==tag*(-1)) s->row=r+1; s->col=c;
else if (w[r][c-1]==tag*(-1)) s->row=r; s->col=c-1;
else if (w[r][c+1]==tag*(-1)) s->row=r; s->col=c+1;
else p->len=0; return p;
push(S,s); visited[s->row][s->col]=1;
flag=search(s,tag);
while ( (2))
{ push(S,s); visited[s->row][s->col]=1;
(3);
}
while (S->top>=0)
{ pop(S);
(4);
flag=search(s,tag);
while (flag)
{ push(S,s);
visit(s);
flag=search(s);
}
}
}
void push( struct stack *S, struct position *s)
{ S->top++;
S->node[S->top].row=s->row;
S->node[S->top].col=s->col;
p->num++;
p->data[p->num].row=s->row;
p->data[p->num].col=s->col;
}
void pop(struct stack *S)
{ S->top--;
}
struct position *gettop(struct stack *S)
{ struct position *s;
s->row=S->data[S->top].row;
s->row=S->data[S->top].row;
return s;
}
int search(struct position *s,int tag)
{ int row,col;
row=s->row; col=s->col;
if (W[row+1][col]=(-1)*tag)&&(!visited[row+1][col])
{ s->row=row+1;s->col=col; return 1;}
if (W[row-1][col]=(-1)*tag)&&(!visited[row-1][col])
{ s->row=row-1;s->col=col; return 1;}
if (W[row][col+1]=(-1)*tag)&&(!visited[row][col+1])
{ s->row=row;s->col=col+1; return 1;}
if (W[row][col-1]=(-1)*tag)&&(!visited[row][col-1])
{ s->row=row;s->col=col-1; return 1}
(5);
}
答案:
(1)strlen(s)+strlen(t) (2)position+strlen(t) (3)target[i]=s[i-strlen(t)]
(4)return a (5)return f(a,b-a)
(6)q=aveage(head->next) (7)p->ave=(head->data+q->ave*q->num)/p->num
(1)j<d[i] (2)data,d[i],j,n (3)num+i*d<n (4)data[j+i*d]<temp (5)data[j]=temp
(1)q[rear]=T (2)front<p (3)q[rear]=T->lchild (4)count++ (5)flag<count
(1)count (2)(x[i]>end[j])&&(x[i]-start[j]<=1) (3)start[j]=x[i] (4)!flag (5)
(1)visited[i][j]=0 (2)flag (3)flag=search(s,tag) (4)s=gettop(S) (5)return 0
转载于:http://www.cnitblog.com/weitom1982/archive/2006/03/30/8298.html