数据结构: 它是研究计算机数据间关系,包括数据的逻辑结构和存储结构及其操作。
数据(Data):数据即信息的载体,是能够输入到计算机中并且能被计算机识别、存储和处理的符号总称。
数据元素(Data Element):数据元素是数据的基本单位,又称之为记录(Record)。一般,数据元素由若干基本项(或称字段、域、属性)组成。
数据的逻辑结构表示数据运算之间的抽象关系。按每个元素可能具有的直接前趋数和直接后继数将逻辑结构分为“线性结构”和“非线性结构”两大类。
线性结构:
一个对一个,如线性表、栈、队列
树形结构:
一个对多个,如树。
图状结构
多个对多个
此外还有集合:
数据元素间除“同属于一个集合”外,无其它关系。
存储结构:逻辑结构在计算机中的具体实现方法。
存储结构是通过计算机语言所编制的程序来实现的,因而是依赖于具体的计算机语言的。
顺序存储(Sequential Storage):
将数据结构中各元素按照其逻辑顺序存放于存储器一片连续的存储空间中。
如c语言的一维数组,如表 L=(a1,a2,……,an)的顺序结构
链式存储(重点)
将数据结构中各元素分布到存储器的不同点,用地址(或链指针)方式建立它们之间的联系。
数据结构中元素之间的关系在计算机内部很大程度上是通过地址或指针来建立的。
索引存储
在存储数据的同时,建立一个附加的索引表,即索引存储结构=数据文件+索引表。
散列存储:
根据数据元素的特殊字段(称为关键字key),计算数据元素的存放地址,然后数据元素按地址存放
线性表是包含若干数据元素的一个线性序列
记为: L=(a0, … ai-1, ai, ai+1 … an-1)
L为表名,ai (0≤i≤n-1)为数据元素;
n为表长,n>0 时,线性表L为非空表,否则为空表。
线性表L可用二元组形式描述:
L= (D,R)
即线性表L包含数据元素集合D和关系集合R
D={ai | ai∈datatype ,i=0,1,2, ∙∙∙∙∙∙∙∙∙n-1 ,n≥0}
R={<ai , ai+1> | ai , ai+1∈D, 0≤i≤n-2}
线性表的特征:
线性表包括顺序表和链表,其中链表又包括单链和双链。线性表具体分类如下:
设有一个顺序表L={1,2,3,4,5,6}; 他们的关系如图:
使用二元组描述L=(D,R),则
D={1 , 2 , 3 , 4 , 5 , 6}(n=6)
R={<1,2> , <2,3> , <3,4> , <4,5> , <5,6>}
若将线性表L=(a0,a1, ……,an-1)中的各元素依次存储于计算机一片连续的存储空间。
设Loc(ai)为ai的地址,Loc(a0)=b,每个元素占d个单元 则:Loc(ai)=b+i*d
在C语言中,可借助于一维数组类型来描述线性表的顺序存储结构:
#define N 100
typedef int data_t;
typedef struct
{ data_t data[N]; //表的存储空间
int last;
} sqlist, *sqlink;
设线性表 L=(a0,a1, ……,an-1),对 L的基本运算有:
1)建立一个空表:list_create(L)
2)置空表:list_clear(L)
3)判断表是否为空:list_empty (L)。若表为空,返回值为1 , 否则返回 0
4)求表长:length (L)
5)取表中某个元素:GetList(L , i ), 即ai。要求0≤i≤length(L)-1
6)定位运算:Locate(L,x)。确定元素x在表L中的位置(或序号)
L o c a t e ( L , x ) = { i , 当元素 x = a i ∈ L , 且 a i 是第一个与 x 相等时 − 1 x 不属于 L 时 Locate(L,x) = \begin{cases} i , &当元素x=ai∈L,且ai是第一个与x相等时 \\ -1 & x不属于L时 \\ \end{cases} Locate(L,x)={i,−1当元素x=ai∈L,且ai是第一个与x相等时x不属于L时
定位:确定给定元素x在表L中第一次出现的位置(或序号)。即实现Locate(L,x)。算法对应的存储结构如图所示。
7)插入:
Insert(L,x,i)。将元素x插入到表L中第i个元素ai之前,且表长+1。
插入前: (a0,a1,—,ai-1,ai,ai+1-------,an-1) 0≤i≤n,i=n时,x插入表尾
插入后: (a0,a1,—,ai-1, x, ai,ai+1-------,an-1)
插入算法思路:若表存在空闲空间,且参数i满足:0≤i≤L->last+1,则可进行正常插入。插入前,将表中(L->data[L->last]~L->data[i])部分顺序下移一个位置,然后将x插入L->data[i]处即可。算法对应的表结构。
8)删除:
Delete(L,i)。删除表L中第i个元素ai,且表长减1, 要求0≤i≤n-1。
删除前: (a0,a1,—,ai-1,ai,ai+1-------,an-1)
删除后: (a0,a1,—,ai-1,ai+1-------,an)
删除:将表中第i个元素ai从表中删除,即实现DeleteSqlist(L, i)。
算法思路: 若参数i满足:0≤i≤L->last, 将表中L->data[i+1]∽L->data[L->last] 部分顺序向上移动一个位置,覆盖L->data[i]。
线性表的顺序存储结构有存储密度高及能够随机存取等优点,但存在以下不足:
(1)要求系统提供一片较大的连续存储空间。
(2)插入、删除等运算耗时,且存在元素在存储器中成片移动的现象;
链表主要学习单链表
将线性表L=(a0,a1,……,an-1)中各元素分布在存储器的不同存储块,称为结点,通过地址或指针建立元素之间的联系
结点的data域存放数据元素ai,而next域是一个指针,指向ai的直接后继ai+1所在的结点。
1.创建结构体
typedef struct node
{ data_t data; //结点的数据域//
struct node *next; //结点的后继指针域//
}listnode, *linklist;
设p指向链表中结点ai
获取ai,写作:p->data;
而取ai+1,写作:p->next->data
若指针p的值为NULL,则它不指向任何结点, 此时取p->data或p->next是错误的。
可调用C语言中malloc()函数向系统申请结点的存储空间
linklist p;
p = (linklist)malloc(sizeof(listnode));
则创建一个类型为linklist的结点,且该结点的地址已存入指针变量p中:
2.建立单链表
依次读入表L=(a0,…,an-1)中每一元素ai(假设为整型),若ai≠结束符(-1),则为ai创建一结点,然后插入表尾,最后返回链表的头结点指针H。
设L=(2,4,8,-1),则建表过程如下:
链表的结构是动态形成的,即算法运行前,表结构是不存在的
3.链表查找
1)按序号查找:实现GetLinklist(h, i)运算。
算法思路:从链表的a0起,判断是否为第i结点,若是则返回该结点的指针,否则查找下一结点,依次类推。
2)按值查找(定位) : 即实现Locate(h, x)。
算法思路:从链表结点a0起,依次判断某结点是否等于x,若是,则返回该结点的地址,若不是,则查找下一结点a1,依次类推。若表中不存在x,则返回NULL。
4.链表的插入
即实现InsertLinklist(h, x, i,)。将x插入表中结点ai之前的情况。
算法思路:调用算法GetLinklist(h, i-1),获取结点ai-1的指针p(ai 之前驱),然后申请一个q结点,存入x,并将其插入p指向的结点之后。
5.链表的删除
即实现DeleteLinklist(h, i), 算法对应的链表结构如图所示。
算法思路:同插入法,先调用函数GetLinklist(h, i-1),找到结点ai的前驱,然后将结点ai删除之。
算法思路:依次取原链表中各结点,将其作为新链表首结点插入H结点之后
设结点data域为整型,求链表中相邻两结点data值之和为最大的第一结点的指针。
算法思路:设p,q 分别为链表中相邻两结点指针,求p->data+q->data为最大的那一组值,返回其相应的指针p即可
设两单链表A、B按data值(设为整型)递增有序,将表A和B合并成一表A,且表A也按data值递增有序。
算法思路:设指针p、q分别指向表A和B中的结点,若p->data ≤q->data则p结点进入结果表,否则q结点进入结果表。
栈是限制在一端进行插入操作和删除操作的线性表(俗称堆栈),允许进行操作的一端称为“栈顶”,另一固定端称为“栈底”,当栈中没有元素时称为“空栈”。
特点 :后进先出(LIFO)。
它是顺序表的一种,具有顺序表同样的存储结构,由数组定义,配合用数组下标表示的栈顶指针top(相对指针)完成各种操作。
1.创建结构体
typedef int data_t ; /*定义栈中数据元素的数据类型*/
typedef struct
{
data_t *data ; /*用指针指向栈的存储空间*/
int maxlen; /*当前栈的最大元素个数*/
int top ; /*指示栈顶位置(数组下标)的变量*/
} sqstack; /*顺序栈类型定义*/
2.创建栈
sqstack *stack_create (int len)
{
sqstack *ss;
ss = (seqstack *)malloc(sizeof(sqstack));
ss->data = (data_t *)malloc(sizeof(data_t) * len);
ss->top = -1;
ss->maxlen = len;
return ss;
}
stack _clear(sqstack *s)
{
s-> top = -1 ;
}
//判断栈是否空 :
int stack_empty (sqstack *s)
{
return (s->top == -1 ? 1 : 0);
}
4.进栈
void stack_push (sqstack *s , data_t x)
{ if (s->top = = N - 1){
printf ( “overflow !\n”) ;
return ;
}
else {
s->top ++ ;
s->data[s->top] = x ;
}
return ;
}
5.出栈
datatype stack_pop(sqstack *s)
{
s->top--;
return (s->data[s->top+1]);
}
//取栈顶元素:
datatype get_top(sqstack *s)
{
return (s->data[s->top]);
}
C语言实现过程如下:
插入操作和删除操作均在链表头部进行,链表尾部就是栈底,栈顶指针就是头指针。
1.创建结构体
ypedef int data_t ; /*定义栈中数据元素数据类型*/
typedef struct node_t {
data_t data ; /*数据域*/
struct node_t *next ; /*链接指针域*/
} linkstack_t ; /*链栈类型定义*/
2.创建空栈
linkstack_t *CreateLinkstack() {
linkstack_t *top;
top = (linkstack_t *)malloc(sizeof(linkstack_t));
top->next = NULL;
return top;
}
//判断是否空栈 :
int EmptyStack (linkstack_t *top)
{
return (top->next == NULL ? 1 : 0);
}
3.入栈
void PushStack(linkstack_t *top, data_t x)
{
linkstack_t *p ;
p = (linkstack_t *)malloc ( sizeof (linkstack_t) ) ;
p->data = x ;
p->next = top->next;
top->next = p;
return;
}
队列是限制在两端进行插入操作和删除操作的线性表
C语言实现:
1.创建结构体
typedef int data_t ; /*定义队列中数据元素的数据类型*/
#define N 64 /*定义队列的容量*/
typedef struct {
data_t data[N] ; /*用数组作为队列的储存空间*/
int front, rear ; /*指示队头位置和队尾位置的指针*/
} sequeue_t ; /*顺序队列类型定义*/
2.创建空队列
sequeue_t *CreateQueue ()
{
sequeue_t *sq = (sequeue_t *)malloc(sizeof(sequeue_t));
sq->front = sq->rear = maxsize -1;
return sq;
}
//判断队列空:
int EmptyQueue (sequeue_t *sq) {
return (sq->front = = sq->rear) ;
}
3.入队
将新数据元素x插入到队列的尾部
void EnQueue (sequeue_t *sq , data_t x)
{
sq->data[sq->rear] = x ;
sq->rear = (sq->rear + 1) % N ;
return ;
}
C语言实现:
1.创建结构体
插入操作在队尾进行,删除操作在队头进行,由队头指针和队尾指针控制队列的操作。
typedef int data_t;
typedef struct node_t
{
data_t data ;
struct node_t *next;
} linknode_t, *linklist_t;
typedef struct
{
linklist_t front, rear;
} linkqueue_t;
2.创建空队列
linkqueue_t *CreateQueue()
{
linkqueue_t *lq = (linkqueue_t *)malloc(sizeof(linkqueue_t));
lq->front = lq->rear = (linklist_t)malloc(sizeof(linknode_t));
lq->front->next = NULL ; /*置空队*/
return lq; /*返回队列指针*/
}
//判断队列空 :
int EmptyQueue(linkqueue_t *lq) {
return ( lq->front = = lq->rear) ;
}
3.入队
void EnQueue (linkqueue_t *lq, data_t x)
{
lq->rear->next (linklist_t)malloc(sizeof(linknode_t)) ;
lq->rear = lq->rear->next; /*修改队尾指针*/
lq->rear->data = x ; /*新数据存入新节点*/
lq->rear->next = NULL ; /*新节点为队尾*/
return;
}
4.出队
data_t DeQueue(linkqueue_t *lq)
{
data_t x;
linklist_t p; /*定义一个指向队头结点的辅助指针*/
p = lq->front->next ; /*将它指向队头结点*/
lq->front->next = p->next ; /*删除原先队头结点
x = p->data;
free(p) ; /*释放原队头结点*/
if (lq->front->next == NULL) lq->rear = lq->front;
return x;
}
树(Tree)是n(n≥0)个节点的有限集合T,它满足两个条件 :
二叉树是n(n≥0)个节点的有限集合,可以是空集(n=0)
也可以是由一个根节点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成。
严格区分左孩子和右孩子,即使只有一个子节点也要区分左右。
二叉树第i(i≥1)层上的节点最多为2i-1个。
深度为k(k≥1)的二叉树最多有2k-1个节点。
满二叉树 : 深度为k(k≥1)时有2k-1个节点的二叉树。
完全二叉树 : 只有最下面两层有度数小于2的节点,且最下面一层的叶节点集中在最左边的若干位置上。
具有n个节点的完全二叉树的深度为
(log2n)+1或 log2(n+1)。
顺序存储结构 : 完全二叉树节点的编号方法是从上到下,从左到右,根节点为1号节点。设完全二叉树的节点数为n,某节点编号为i。
有n个节点的完全二叉树可以用有n+1个元素的数组进行顺序存储,节点号和数组下标一一对应,下标为零的元素不用。
利用以上特性,可以从下标获得节点的逻辑关系。不完全二叉树通过添加虚节点构成完全二叉树,然后用数组存储,这要浪费一些存储空间。
C语言实现创建结构体
typedef int data_t ;
typedef struct node_t;
{
data_t data ;
struct node_t *lchild ,*rchild ;
} bitree_t ;
bitree_t *root ;
遍历 : 沿某条搜索路径周游二叉树,对树中的每一个节点访问一次且仅访问一次。
二叉树是非线性结构,每个结点有两个后继,则存在如何遍历即按什么样的搜索路径进行遍历的问题。
由于二叉树的递归性质,遍历算法也是递归的。三种基本的遍历算法如下 :
先序遍历算法
void PREORDER ( bitree *r)
{
if ( r = = NULL ) return ; //空树返回
printf ( “ %c ”,r->data ); //先访问当前结点
PREORDER( r->lchild ); //再访问该结点的左子树
PREORDER( r->rchild ); //最后访问该结点右子树
}
中序遍历算法
若二叉树为空树,则空操作;否则中序遍历左子树。
当访问根结点,中序遍历右子树。
void INORDER ( bitree *r)
{
if ( r = = NULL ) return ; //空树返回
INORDER( r->lchild ); //先访问该结点的左子树
printf ( “ %c ”,r->data ); //再访问当前结点
INORDER( r->rchild ); //最后访问该结点右子树
}
后序遍历算法
若二叉树为空树,则空操作;否则后序遍历左子树
当访问根结点,后序遍历右子树。
void POSTORDER ( bitree *r)
{
if ( r = = NULL ) return ; //空树返回
POSTORDER( r->lchild ); //先访问该结点的左子树
POSTORDER( r->rchild ); //再访问该结点右子树
printf ( “ %c ”,r->data ); //最后访问当前结点
}
遍历的路径相同,均为从根节点出发,逆时针沿二叉树的外缘移动,每个节点均经过三次。按不同的次序访问可得不同的访问系列,每个节点有它的逻辑前趋(父节点)和逻辑后继(子节点),也有它的遍历前趋和遍历后继(要指明遍历方式)。
按编号遍历算法 :
NOORDER ( bitree *r) /*按编号顺序遍历算法*/
{
int front, rear;
bitree *Q[N];
if ( r == NULL ) return ; /*空树返回*/
for (rear=1;rear<N; rear++) Q[rear] = NULL ;
front = rear = 1; Q[rear] = r;
while ( Q[front] != NULL ) { /*以下部分算法由学生完成设计*/
/*访问当前出队节点*/
/*若左孩子存在则左孩子入队*/
/*若有孩子存在则右孩子入队*/
/* front向后移动*/
}
}
查找:
设记录表L=(R1 R2……Rn),其中Ri(l≤i≤n)为记录,对给定的某个值k,在表L中确定key=k的记录的过程,称为查找。
若表L中存在一个记录Ri的key=k,记为Ri.key=k,则查找成功,返回该记录在表L中的序号i(或Ri 的地址),否则(查找失败)返回0(或空地址Null)。
对查找算法,主要分析其T(n)。查找过程是key的比较过程,时间主要耗费在各记录的key与给定k值的比较上。比较次数越多,算法效率越差(即T(n)量级越高),故用“比较次数”刻画算法的T(n)。
一般以“平均查找长度”来衡量T(n)。
平均查找长度ASL(Average Search Length):对给定k,查找表L中记录比较次数的期望值(或平均值),即:
Pi为查找Ri的概率。等概率情况下Pi=1/n;Ci为查找Ri时key的比较次数(或查找次数)。
顺序表,是将表中记录(R1 R2……Rn)按其序号存储于一维数组空间
记录Ri的类型描述如下:
typedef struct
{
keytype key; //记录key//
…… //记录其他项//
} Retype;
顺序表类型描述如下:
#define maxn 1024 //表最大长度//
typedef struct
{
Retype data[maxn]; //顺序表空间//
int len; //当前表长,表空时len=0//
} sqlist;
若说明:sqlist r,则(r.data[1],……,r.data[r.len])为记录表(R1……Rn), Ri.key为r.data[i].key, r.data[0]称为监视哨,为算法设计方便所设。
算法思路: 设给定值为k,在表(R1 R2……Rn)中,从Rn开始,查找key=k的记录。
int sqsearch(sqlist r, keytype k)
{
int i;
r.data[0].key = k; //k存入监视哨//
i = r.len; //取表长//
while(r.data[i].key != k) i--;
return (i);
}
设Ci(1≤i≤n)为查找第i记录的key比较次数(或查找次数):
若r.data[n].key = k, Cn=1;
若r.data[n-1].key = k, Cn-1=2;
……
若r.data[i].key = k, Ci=n-i+1;
……
若r.data[1].key = k, C1=n
故ASL = O(n)。而查找失败时,查找次数等于n+l,同样为O(n)。
算法思路:
对给定值k,逐步确定待查记录所在区间,每次将搜索空间减少一半(折半),直到查找成功或失败为止。
设两个游标low、high,分别指向当前待查找表的上界(表头)和下界(表尾)。mid指向中间元素。
现查找k=20的记录。
再看查找失败的情况,设要查找k=85的记录。
C语言实现如下:
int Binsearch(sqlist r, keytype k) //对有序表r折半查找的算法//
{
int low, high, mid; low = 1;high = r.len;
while (low <= high)
{
mid = (low+high) / 2;
if (k == r.data[mid].key)
return (mid);
if (k < r.data[mid].key)
high = mid-1;
else
low = mid+1;
}
return(0);
}
不失一般性,设表长n=2h-l,h=log2(n+1)。记录数n恰为一棵h层的满二叉树的结点数。得出表的判定树及各记录的查找次数如图所示。
设记录表长为n,将表的n个记录分成b=[n/s]个块,每块s个记录(最后一块记录数可以少于s个),即:
且表分块有序,即第i(1≤i≤b-1)块所有记录的key小于第i+1块中记录的key,但块内记录可以无序。
步骤:
稳定排序和非稳定排序
设文件f=(R1……Ri……Rj……Rn)中记录Ri、Rj(i≠j,i、j=1……n)的key相等,即Ki=Kj。
若在排序前Ri领先于Rj,排序后Ri仍领先于Rj,则称这种排序是稳定的,其含义是它没有破坏原本已有序的次序。
内排序和外排序
设待排文件f=(R1 R2……Rn)相应的key集合为k={k1 k2……kn},
排序方法:
先将文件中的(R1)看成只含一个记录的有序子文件,然后从R2起,逐个将R2至Rn按key插入到当前有序子文件中,最后得到一个有序的文件。插入的过程上是一个key的比较过程,即每插入一个记录时,将其key与当前有序子表中的key进行比较,找到待插入记录的位置后,将其插入即可。
设文件记录的key集合k={50,36,66,76,95,12,25,36}
排序算法的T(n)=O(n2),是内排序时耗最高的时间复杂度。
折半插入排序方法
先将(R[1])看成一个子文件,然后依次插入R[2]……R[n]。但在插入R[i]时,子表[R[1]……R[i-1]]已是有序的,查找R[i]在子表中的位置可按折半查找方法进行,从而降低key的比较次数。
设待排序文件f=(R1 R2……Rn),对应的存储结构为单链表结构
链表插入排序实际上是一个对链表遍历的过程。先将表置为空表,然后依次扫描链表中每个结点,设其指针为p,搜索到p结点在当前子表的适当位置,将其插入。
设含4个记录的链表如图:
设记录的key集合k={50,36,66,76,36,12,25,95},每次以集合中第一个key为基准的快速排序过程如下: