01--单向链表

594 阅读7分钟

# 01--数据结构与算法基础知识已经介绍了关于数据结构与算法的基础知道,需要了解的朋友请移步。

一、 单向链表的设计

链式存储的特性:

  • 有唯一的第一个元素和最后一个元素;
  • 除第一个元素和最后一个元素外的其他元素都有一个前驱和一个后续
  • 第一个元素没有前驱,只有后续,最后一个元素有前驱,没有后续。

单向链表采用了链式存储的方式来实现,链表中的各个结点的内存地址是不连续的,我们可以通过当前结点找到下一个结点。结合线性结构和链式存储的特性,我们来设计单向链表。

二、准备

设计一些状态值和数据类型作为函数调用的状态回调

#define ERROR 0

#define TRUE 1

#define FALSE 0

#define OK 1

#define MAXSIZE 20 /* 存储空间初始分配量 */

typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */

typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */

三、节点的设计

单向链表的特性:

  • 唯一第一个节点和最后一个节点,我们称之为首元节点尾节点
  • 除首元节点和尾节点以外的其它节点都有一个前驱和一个后续
  • 首元节点只有后续,没有前驱;
  • 尾节点只有前驱,没有后续。

单向链表的节点包含数据域指针域,数据域记录该节点需要记录的数据,指针域指向下一个节点:

image.png

根据单向链表的特性,我们设计如下数据结构作为单向链表的节点:

typedef struct Node{

    ElemType data; //数据域
    struct Node *next; //指针域
}Node;

typedef struct Node * LinkList; //节点类型重命名

在设计单向链表时,有两种设计方案:带头节点不带头节点。所谓的头节点就是在链表的最前面放置一个不记录任何数据的节点,通过头节点来方便首元节点的插入、删除等操作。

1.不带头节点

image.png

2.带头节点

image.png

注:后面的代码逻辑都是基于带头节点的单向链表来实现的。

四、创建

Status InitList(LinkList *L){

    //产生头节点,并使用L指向此头节点
    *L = (LinkList)malloc(sizeof(Node));

    //存储空间分配失败
    if(*L == NULL) return ERROR;

    //将头结点的指针域置空
    (*L)->next = NULL;
    
    return OK;
}

创建单向链表时,通过malloc创建一个节点,将*L指向这个节点,并将节点的next指向NULL,此时单向链表只有一个节点,它即是首元节点也是尾结点。

在创建链表时可能会插入多个数据,而插入数据的方式一般是在链表的头部插入和尾入插入两种方式,我们称之为头插法和后插法

1、前插法

image.png

如上图所示,前插法的意思就是把新插入的元素插入到第一个位置,也就是首元节点的位置,使之成为新的首元节点。

代码实现:

void CreateListHead(LinkList *L, int n){
    LinkList p;

    //创建一个带头结点的单链表
    *L = (LinkList)malloc(sizeof(Node));
    (*L)->next = NULL;

    //循环前插入随机数据
    for(int i = 0; i < n;i++)
    {
        //生成新结点
        p = (LinkList)malloc(sizeof(Node));
        
        //i赋值给新结点的data
        p->data = i;

        //新结点的next指向原来的首元节点:p->next = 头结点的L->next
        p->next = (*L)->next;

        //头节点的next指向新节点,使得新节点成为首元节点
        (*L)->next = p;
    }
}

使用前插法插入的数据都会插入在首元节点的位置,所以最终得到的单向链表是一个逆序的链表。

2、后插法

image.png

如上图所示,后插法就是指新插入的数据插入到链表的尾部,为方便插入,每次插入后都需要记录尾节点,这样下次插入数据时即可直接将原来的尾节点的next指向插入的新节点,使得新节点成为新的尾节点。

代码实现:

void CreateListTail(LinkList *L, int n){
    LinkList p,r;

    //创建一个带头节点的单链表
    *L = (LinkList)malloc(sizeof(Node));

    //r指向尾部的结点
    r = *L;

   for (int i=0; i<n; i++) {
        //生成新节点
        p = (Node *)malloc(sizeof(Node));
        p->data = i;

        //将尾点的指针指向新节点,这样新节点就变成新的尾节点了
        r->next = p;

        //记录尾节点,方便下一次插入新数据
        r = p;
    }
    
    //插入完成后需要将尾指针的next = NULL
    r->next = NULL;
}

使用后插法插入的数据都会插入在尾节点的位置,所以最终得到的单向链表是一个顺序的链表。

五、插入指定位置

image.png

假如要在Hank节点所在的位置插入一个节点Cooci,则需要先找到CC节点。创建一个新节点Cooci,Cooci节点的next指向Hank节点,CC节点的next指向Cooci节点。

代码实现

Status ListInsert(LinkList *L,int i,ElemType e){
    int j;
    LinkList p,s;

    p = *L;
    j = 1;

    //找到要插入位置的前一个节点
    while (p && j<i) {
        p = p->next;
        ++j;
    } 

    //要插入的位置不存在节点,插入无效
    if(!p || j>i) return ERROR;

    //生成新节点s
    s = (LinkList)malloc(sizeof(Node));
    s->data = e;//将e赋值给s的数值域

    //新节点的next指向目标位置的原节点
    s->next = p->next;

    //目标位置的前一个节点的next指向新节点
    p->next = s;
    
    return OK;
}

头节点的重要性在给单向链表插入数据时就体现出来了。插入数据最关键的就是要找到目标位置的前驱,有头节点的情况下,包括首元节点在内的所有节点都有前驱,所以对于任何位置的插入,代码实现时的逻辑都可以统一编写了。

六、删除

image.png

如果要删除Hank节点,则需要找到Hank节点和它的前驱CC节点,将CC节点的next指向Hank节点的next节点Cooci节点,再将Hank节点释放。

Status ListDelete(LinkList *L,**int** i,ElemType *e){
    int j;
    LinkList p,q;

    p = (*L)->next;
    j = 1

    //找到目标节点的前一个节点
    while (p->next && j<(i-1)) {
        p = p->next;
        ++j;
    }

    //当i>n 或者 i<1 时,删除位置不合理
    if (!(p->next) || (j>i-1)) return  ERROR;

    //q指向要删除的结点
    q = p->next;

    //目标节点的前驱的next指向目标节点的后续
    p->next = q->next;

    //将q结点中的数据给e
    *e = q->data;

    //让系统回收此结点,释放内存;
    free(q);

    return OK;
}

删除节点时最关键的也是找到目标节点的前驱,这里也体现了设计头节点的重要性。

七、查询

Status GetElem(LinkList L,int i,ElemType *e){
    //j: 计数.
    int j;

    //声明节点p;
    LinkList p;

    //将节点p 指向链表L的首元节点;
    p = L->next;

    //j计算=1;
    j = 1;

    //找到目标位置的节点:p不为空,且计算j不等于i,则循环继续
    while (p && j<i) {
        //p指向下一个节点
        p = p->next;
        ++j;
    }

    //如果p为空或者j>i,则返回error
    if(!p || j > i) return ERROR;
     
    //e = p所指的结点的data
    *e = p->data;

    return OK;
}

查询最关键的判断最终是否找到了对应的节点

八、遍历

Status ListTraverse(LinkList L)

{
    LinkList p=L->next;

    while(p)
    {
        printf("%d\n",p->data);
        p=p->next;

    }
    printf("\n");

    return OK;
}

遍历时通过当前节点的next找到下一个节点,当next指向为NULL时,说明遍历完成。

九、清空

Status ClearList(LinkList *L)
{

    LinkList p,q;
    p=(*L)->next;           /*  p指向第一个结点 */

    while(p)                /*  没到表尾 */
    {
        q=p->next;
        free(p);
        p=q;
    }

    (*L)->next=NULL;        /* 头结点指针域为空 */

    return OK;
}

清空链表时,从首元节点开始,先找到当前节点的后续,再释放当前节点;循环操作,直到当前节点的为NULL时结束。最后需要将头节点的next指向NULL

十、总节

在设计单向链表时设计一个头节点,可以帮助我们设计出更高效的删除插入算法;前插法后插法也是单向链表的重点