严蔚敏版数据结构学习笔记(2):线性表的链式表示和实现

来源:互联网 发布:天行去广告软件 编辑:程序博客网 时间:2024/05/18 01:30

线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的也可以是不连续的)。也就是通过指针实现了物理不相邻的存储结构存放逻辑上相邻的数据元素。对线性表的一个数据元素ai来说,除了要存储其本身的数据信息之外,还需要存储一个指示其直接后继的信息(一般来说是一个指针信息)。这两部分组成数据元素ai的存储映象,称为结点(node).他有两个域,存储数据元素的信息的数据域和存储直接后继的指针域。指针域中存储的信息称为指针或者链。n个结点(ai(1<=i<=n))链结成一个链表,既是线性表的链式存储结构。又因为这种链表的结点中只包含了一个指针域,所以又称线性链表或者单链表;
整个链表的存取必须从头指针开始进行,头指针指向链表中的第一个结点的存储位置。链表的最后一个元素没有直接后继,所以最后一个结点的指针为“空”(NULL);
通过结构体LNode来定义这个数据结构

typedef struct LNode{    ElemType data;    struct LNode *next;}LNode *LinkList;

单链表可以由头指针唯一确定。我们可以在定义单链表的时候在单链表的第一个结点之前附设一个结点,称为头结点。这样如果单链表为空可以直接“head->next=NULL”即可。
假设p值指向线性表中第i个数据元素ai,则p->next指向第ai+1个数据元素的指针。也就是或p->data = ai;p->next->data = ai+1;所以在单链表中要取的第i个数据元素必须从头指针出发。
上一篇博客我实现了线性表的顺序结构的GetElem():

Stutas GetElem(SqList L,int i,ElemType e){//初始条件:顺序线性表L已存在,1≤i≤ListLength(L)用e返回L中第i个数据元素的值,注意i指位置,第1个位置的数组是从0开始    if(L.length==0||i<0||i>L.length) return ERROR;    else{        *e = L.elem[i-1];        return OK;    }}

下面对比一下GetElem()在单链表中的实现:

Status GetElem(LinkList L,int i,ElemType &e){    LinkList p;    p = L->next;//// 初始化,p指向第一个结点,j为计数器    int j = 1;    while(p&&j<i){    //顺指针向后查找,直到p指向第i个元素或p为空        p = p->next;        ++j;    }    if(j>=i||!p) return ERROR;    e = p->data;    return OK;}

该算法的时间复杂度为O(n);
我们在顺序结构的时候重点的理解了线性表的顺序结构如何插入和删除操作。那么我们在单链表中又怎么实现这两种操作呢。
我们先来看一下插入的操作:

假设我们要在线性表的两个元素a,b之间插入一个数据元素x,已知指针p指向结点a,s指向结点x。要想完成插入操作,必须有如下的步骤:(1)将a这个结点的后继赋给s的后继,(2)将s赋值给a结点的后继。
用语句表述即为:

s->next = p->next;p->next=s; 

反之,如果要删除如图ai结点时
这里写图片描述
假设p为指向ai-1结点的指针,删除ai结点只有一个步骤:将ai的后继赋值给ai-1:

p->next=p->next->next;

可以通过该语句知道在进行删除操作的时候是可以不用移动数据元素的。
同样的,在顺序结构的ListInsert()和ListDelete()算法也可以在单链表中实现:
ListInsert():

Status ListInsert(LinkList &L,int i,ElemType e){//在带头结点的单链表L中的第i个元素之前的位置插入元素e     LinkList p,s;    p = L;    int j = 0;    while(p&&j<i-1){//寻找第i-1个结点         p = p->next;        ++j;    }    if(j>i-1||!p) return ERROR;//i小于1或者大于表长加1,报错    s = (LinkList )malloc(sizeof(LNode));    s->data = e;    s->next = p->next;    p->next = s;    return OK;}  

ListDelete():

Status ListDelete(LinkList &L,int i,Elemtype &e){//在带头结点的单链表L中,删除第i个元素,并由e返回其值    LinkList p,q;    int j = 0;    while(p&&j<i){//寻找第i个结点,并令p指向其前驱         p = p->next;        ++j;    }     if(!(p->next)||j>i-1) return ERROR;//删除位置不合理    q = p->next; p->next = q->next;//删除并释放结点    e = q->data;    free(q);    return OK;}

很容易可以看出上面的两个算法的时间复杂度都是O(n),我们只需要一次循环找出我们要删除或者插入的位置即可;
可以看到在ListInsert()算法中重新建立了单链表s,我们也可以把创建单链表的操作写成一个新的算法CreatList():

void Creat(LinkList &L,int n){//逆序为输入n个元素的值,建立带表头结点的单链表L     LinkList p;    int i;    L = (LinkList)malloc(sizeof(LNode));    L->next = NULL;//先建立一个带头结点的单链表    for(i=n;i>0;i--){        p = (LinkList)malloc(sizeof(LNode));//生成新结点         scanf(&p->data);//输入元素值         p->next = L->next;        L->next = p;//插入到表头 ,头结点依然要指向第一个结点     }  }

下面我们依然来试着顺序结构中写过的MergeList()算法在单链表中的实现:

void MergeList(LinkList &LA,LinkList &LB,LinkList &LC){    LinkList pa,pb,pc;    pa = LA->next;    pb = LB->next;    LC = pc = LA;//用LA的头结点作为LC的头结点     while(pa&&pb){        if(pa->data<=pb->data){            pc->next = pa;            pc = pa;            pa = pa->next;        }else{            pc->next = pb;            pc = pb;            pb = pb->next;        }        pc->next = pa?pa:pb; // 插入剩余段        free(LB);     } } 

可以看出来顺序结构和链式结构的MergeList的时间复杂度是相同的,但是空间复杂度不同。归并两个链表为一个链表时,不需要另建新表的结点空间,只需要将原来的两个链表中的结点之间的关系解除,在重新连接成一个新的表即可 。
有的时候也可以用一维数组来表述线性链表:

#defined MAXSIZE 100typedef struct {    ElemType data;    int cur;}component,SLinkList[MAXSIZE]; 

在此链表结构中,数组的一个分量表示一个结点,同时用一个游标:cur来代替指针指示结点在数组中的相对位置。数组的第零分量可以看作是头结点。这种链表叫做静态链表。
假设有一个S为SLinkList类型的变量,S[0].cur指示为S的头结点在数组中的位置,若 i = S[0].cur,则S[i].data代表着链表的第一个元素。在此种链表中,整型游标i代替动态指针p,i=S[i].cur的操作实为指针后移(类似p = p->next)
我们来用静态链表来实现LocateElem():

int LocateElem(SLinkList S,ElemType e){    int i;    i = S[0].cur;//i指示表中的第一个结点     while(i&&S[i].data != e){        i = S[i].cur;//在表中顺链查找     }    return i; }

通过对静态链表的熟练理解,我们可以在静态链表中实现有关插入和删除的操作,其中插入算法的实现如下:

Status SListInsert(SLinkList &S,int i,ElemType e){//静态链表实现插入算法 ,在链表的第i个数据元素前插入元素e     int j=1,m,s;    int k = S[0].cur;    if(i<1||i>SLinkList(S)-1) return ERROR;//不符合要求,退出    while(k&&j<i-1){//寻找第i-1个结点         ++j;        k = S[i].cur;    }     m = S[k].cur;//m指向第i个位置    s = Malloc(S);    S[s].data = e;    S[s].cur = m;    S[k].cur = s;//把s给第i-1的一个结点     return OK;}

但是该算法需要我们单独申请一个新的结点,因为我们定义结构体的时候没有指针,不能自动生成空间,所以需要我们单独申请一个空闲的空间出来供我们的新结点使用:此时就需要定义一个新的只和静态链表有关的Malloc()方法来实现:

int Malloc(SLinkList S){// 释放备用链表第一个结点    int i = S[0].cur;    if(i==0) return ERROR;//备用链表为空,退出     else{        S[0].cur = S[i].cur;//因为没有指针,没有动态分配函数,所以要自己实现,分配一个空结点,备用链表头结点,指向备用链表第二个结点        return i;     }   }

那么静态链表的插入算法就实现了,现在我们来试试删除算法:

Status SLinkList(SLinkList &S,int i,ElemType e){//将S中的第i个元素删除,并用e返回其值    int j = 1,m;    int k = S[MAXSIZE-1].cur;    if(i<1||i>SLinkList(S)-1) return ERROR;    while(i&&j<i-1){        ++j;        k = S[k].cur;    }      m = S[k].cur;    *e = S[m].data;    S[k].cur = S[m].cur;//这样的话其实是吧i-1的cur附上了i的cur,直接用cur跳过了这个元素,也就是说如果i的cur是3,那么i-1的cur被赋值成3之后后面链接的部分就是剩余部分     free(L,m);} 

当我们找到我们要删除的结点前的一个结点之后,我们用一个新的变量来存储前一个结点的游标,也就是我们要删除的那一个结点。然后用要删除的前结点的cur赋值给前一个结点的cur,那么就实现了对这个结点的删除,也就是通过改变前一个结点的游标来隐藏这个结点,然后在释放这个结点,就实现了对该结点的删除。那么我们怎么来释放这个结点呢,毕竟申请空间是我们自己申请的,释放当然也要自己“动手”咯,我们定义如下的释放结点的方法:

void free(SLinkList S,int i){//也就是在把删除的元素放在头结点之后,放在第一个结点之前     S[i].cur = S[0].cur;    S[0].cur = i;} 

通过这个代码我们可以发现我们所谓的释放也就是这个结点放在我们这个结点的头结点之后,第一个结点之前的位置,就实现的这个结点“隐藏”,也就是释放功能;
当然一切的一切都得靠我们先重新定义一个备用的链表空间,我们可以这么定义:

void InitSpace(SLinkList &space){    //将一维数组space中的各分量连成一个备用链表,,space[0].cur为头指针,“0”表示空指针    for(int i=0;i<MAXSIZE-1;++i){        space[i].cur = i+1;    }     space[MAXSIZE-1].cur = 0;} 

这样一个基础静态链表应有的部分都已经齐全了,关于一些别的链表计算方法我有时间就会上载到这篇博客上的。
我先留一个ADT SLinkList{};里面的方法我会一一实现然后上载的。
循环链表
循环链表是另一种形式的链式存储结构,他的特点是表中最后一个结点的指针域指向头结点,使之整个链表连成一个环;循环链表的基本操作和单链表的操作基本是相同的,差别仅仅是算法中的循环条件不是当p或者p->next是否非空的时候,而是他们是否等于头结点。如果要合并两个线性表的话循环链表的优势就被大大提升了,只需要将一个表的尾结点和另一个的表头连接即可;
双向链表
双向链表的结点中有两个指针域,其一指向直接后继,另一个指向直接前驱;

typedef struct DuNode{    ElemType data;    struct DuNode *prior;    struct DuNode *next;}DuLNode,*DuLinkList;

该结构其实也就是一个升级版的单链表,我们只要记住结点p:
p->next->prior=p->prior->next=p;即可
我们通过双向链表的插入算法来熟悉这类双向链表;

Status ListInsert(DuLinkList &L,int i,ElemType e){//在双向链表的第i个数据元素的位置之前插入一个新的元素e//i的合法值是1<=i<=表长加1,每次算法都要判断,否则会出现指针越界的情况     DuLinkList s,p;    if(!(p=GetElem(L,i))) return ERROR;//确定i插入位置,在此需要重写GetElem()方法     if(!(s=(DuLinkList)malloc(sizeof(DuLNode)))) return ERROR;    s->data = e;    s->prior = p->prior;    p->prior->next = s;    s->next = p;    p->prior = s;    return OK;} 

这里要注意连接指针域时要把两者之前的所有连接连上,因为连接其实是双向的,不能断了,比如s->next = p;其实已经将两者连接在一起了,但是只有s的后继连接到了p,p的前驱却没有连接到s,所有还应该有p->prior = s;这个算法才能称作是完整的。
删除算法我会在明天继续上载,今天已经蛮晚了,休息了。

剩余的部分会在接下来几天继续更新(此博客仅仅是严蔚敏版数据结构的学习笔记和算法代码的实现,没有更多的内容。当我把这本书的东西都掌握了我会再更新一些数据结构的习题和扩展)。

阅读全文
0 0
原创粉丝点击