链表

来源:互联网 发布:mac双系统默认windows 编辑:程序博客网 时间:2024/05/20 12:24


一.单链表的定义

线性表的链式存储又称为单链表,它是通过一组任意的存储单元来存储线性表中的数据元素。为了建立起数据元素之间的线性关系,对每个链表结点,除了存放元素自身的信息外,还需要存放一个指向其后继的指针。 
单链表结点的结构如下图所示,其中,data为数据域,存放数据元素;next为指针域,存放其后继结点的地址。

定义

对于每一个结点的描述如下:

typedef struct LNode{    ElemType data;    struct LNode *next;}LNode, *LinkList;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

利用单链表在解决顺序表需要大量的连续存储空间的缺点的同时,也引入了一些不可避免的缺点。比如因为需要额外的指针域,因此需要额外的存储空间;由于单链表是离散的分布在存储空间中,所以单链表不能完成随机存取的操作。 
为了方便标识一个单链表,我们一般需要引入头指针来操作整个单链表。此外,为了统一增加和删除元素的操作,我们一般会在单链表的第一个结点之前附加一个结点,称为头结点。头结点的指针域指向单链表的第一个元素结点。 
注:这里应该注意区分头指针和头结点。而不管单链表有没有头结点,头指针总是指向单链表的第一个结点。简单说就是如果单链表有头结点,那么头指针将指向头结点;如果单链表没有头结点,头指针将指向单链表的第一个结点。此处我们应该注意到一般情况下头结点内不存储任何信息。这里说明下,如果后面的例题中没有具体说明,一般都是建立有头结点的单链表。

单链表

引入头节点后,可以带来两个优点:

  • 由于开始节点的位置被存放在头节点的指针域中,所以在链表的第一个位置上操作与表其他位置上的操作一致,无需进行特殊处理。
  • 无论链表是否为空,其头指针都是指向头节点的非空指针(在空表中,头节点的指针域为空),因此也使得空表和非空表的处理方式变得统一。

二.单链表基本操作的实现

2.1建立单链表

2.1.1头插法建立单链表

该方法中,首先建立一个具有头结点的空单链表,然后每生成一个读取到数据的新节点,就将其放置到头结点之后。如下图所示: 
头插法建立单链表

算法描述如下:

typedef int ElemType;LinkList CreateLink(LNode *head){    LNode *s;    ElemType x;    scanf("%d",&x);    while(x!=999){        s=(LNode*)malloc(sizeof(LNode));        s->data=x;        s->next=head->next;        head->next=s;        scanf("%d",&x);    }    return head;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

注:采用头插法建立单链表,读入数据的顺序与生成的链表中的元素的顺序刚好是相反的。 
每个结点插入的时间复杂度为O(1),设单链表长为n,则总的时间复杂度为O(n)

2.1.2尾插法建立单链表

该方法中同样首先建立一个具有头结点的空单链表,然后每生成一个读取到数据的新节点,就将它插入到表尾;为了达到这样的目的,必须增加一个尾指针r,使其始终指向当前链表的尾结点。如下图所示

尾插法建立单链表

算法描述如下

LinkList CreatLink(LNode *head){       LNode *s;    LNode *r=head;    ElemType x;    scanf("%d",&x);    while(x!=999){        s=(LNode*)malloc(sizeof(LNode));        s->data=x;        r->next=s;        r=s;        scanf("%d",&x);    }    s->next=NULL;    return head;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

注:因为附加设置了一个指向表尾的尾指针r,因此每个结点插入的时间复杂度同样为O(1),设单链表长为n,则总的时间复杂度为O(n)

2.2按序号查找结点值

从单链表的第一个结点出发,顺着指针next域逐个从上往下搜索,直到找到第i个结点为止,否则返回最后一个结点指针域NULL。 
算法描述如下:

LNode* GetElem(LNode *head, ElemType i){    LNode *p=head->next;    int j=1;    if(i==0){        printf("The Link is empty!\n");        return head;    }    if(i<=0){        printf("The postion is illegal!\n");        return NULL;    }    while(p){        if(j==i){            printf("Find success!\n");            break;        }        p=p->next;        j++;    }    return p;   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

注:按序号查找的时间复杂度为O(n)

2.3按值查找结点值

从单链表的第一个结点开始,由前往后依次比较各结点数据域的值,若某结点数据域的值等于给定值x,则返回该结点的指针。若整个单链表中没有这样的结点,则返回NULL。 
算法描述如下:

LNode* GetElem(LNode *head, ElemType x){    LNode *p=head->next;    if(head==NULL){        printf("The LinkList is empty!\n");        return head;    }    while(p){        if(p->data==x){            printf("Find the number!\n");            return p;        }        p=p->next;      }    printf("Not Find the number!\n");    return NULL;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

注:按值查找的时间复杂度为O(n)

2.4插入结点

2.4.1插入后继结点

插入操作是将值为x的新结点插入倒单链表的第i个位置上。先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i1个结点,再在其后插入新的结点。 
算法首先需要调用GetElem(L,i-1)查找第i1个结点。假设返回的第i1个结点为*p,然后令新结点*s的指针域指向*p的后继结点,再令结点*p的指针域插入新的结点*s。其操作过程如下图所示:

插入后继结点

算法描述如下:

LNode* GetElem(LNode *head, int i){    if(i==0){        printf("The LinkList is empty!\n");        return head;    }    if(i<=0){        printf("The LinkList is illegal!\n");        return NULL;    }    LNode *p=head->next;    int j=1;    while(p){        if(j==i){            printf("Find it!The postion is %dth\n", j);            break;        }        j++;        p=p->next;    }    return p;}void InertElem(LNode *p, ElemType x){    LNode *s;    s=(LNode*)malloc(sizeof(LNode));    s->data=x;    s->next=p->next;    p->next=s;}
  • 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
  • 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

算法中,时间的主要开销是查找第i1个元素,时间复杂度为O(n)。若是在给定的结点后面插入新结点,则时间复杂度仅为O(1)

2.4.1插入前驱结点

在方法一中,我们可以在指针p指向的结点后面插入新的结点s,但是有时如果我们需要在指针p指向的结点前面插入新的结点s时,上述算法明显是办不到的。 
但是如果我们换个思路,将指针p指向的结点和s结点它们之间的数据域做一次交换,依旧将s结点插入到指针p指向的结点后面,如此我们便将前插操作变为了向指针p指向的结点的后插操作,并且在逻辑上仍旧满足条件。 
算法描述如下:

void InertElem(LNode *p, ElemType x){    LNode *s;    s=(LNode*)malloc(sizeof(LNode));    s->data=x;    ElemType temp;    temp=p->data;    p->data=s->data;    s->data=temp;    s->next=p->next;    p->next=s;  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

注:同方法一相同,时间的主要开销是查找第i1个元素,时间复杂度为O(n)。若是在给定的结点后面插入新结点,则时间复杂度仅为O(1)

2.5删除结点

2.5.1删除后继结点

删除结点操作即将单链表的第i个结点删除。先检查删除位置的合法性,然后查找表中第i1个结点(即将要被删除结点的前驱结点),然后在删除第i个结点。其操作过程如下图所示:

删除结点

假设我们要删除指针q指向的结点,那么我们首先通过遍历所有结点找到指针q指向的结点的前驱结点p,为了实现算法,我们只需修改指针p的指针域,将指针p的指针域next直接指向指针q指向的结点的指针域next所指的下一个结点便可。 
算法描述如下:

void DelLNode(LNode *head, int i){    LNode *p=head;    LNode *q=head->next;    while(i){        p=q;        q=q->next;        i--;    }    p->next=q->next;    free(q);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

注:删除操作中,时间的主要开销是查找第i1个元素,因此时间复杂度为O(n)。若是删除给定结点的后继结点,则时间复杂度为O(1)

2.5.2删除自身结点

有时我们需要删除指针所指向的自身结点(比如指针p指向的结点),此时如果继续使用上述方法明显是不可能的。我们采用与2.4.1插入前驱结点相似的方法,将指针p所指向的结点的数据域与指针q所指向的结点的数据域进行一次交换(因为是一次删除操作,我们只需要将指针q指向的结点的数据域直接赋值为指针p指向的结点的数据域便可),这样,我们就又变成了删除指针q指向的结点的操作。 
算法描述如下:

void DelList(LNode *head, int i){    LNode *p=head;    LNode *q=p->next;    while(i){        p=q;        q=q->next;        i--;    }    p->data=q->data;    p->next=q->next;    free(q);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

注:与上述算法一样,删除操作中,时间的主要开销是查找第i个元素,因此时间复杂度为O(n)。若是删除给定的结点,则时间复杂度为O(1)

2.5求链表长度

求表长实际上就是计算单链表中数据结点(不含头节点)的个数。为了达到这个目的,我们只需对链表进行一次遍历,同时设置计数器对每次访问到的结点计数便可。 
算法描述如下:

int LenLink(LNode *head){    int cnt=0;    LNode *p=head->next;    while(p){        p=p->next;        cnt++;    }    return cnt;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注:遍历操作中需要访问所有结点,因此时间复杂度为O(n)

原创粉丝点击