List | 链表实现表(单向链表)

目录 | Content

List | 链表实现表(单向链表)_第1张图片

一、概述

表 (List ADT)

我们把一连串数字的排列这种数据类型称为表。比如:A1,A2,A3,A4…An这一组数,我们可以称它为大小为n的表。大小为0的表我们称之为空表(empty lists)。

对于一般的表,我们称An为An-1的后继元(successor),An为An+1的前驱元(predecessor)。

其对应操作有:
CreatList
Insert
Delete
Find
IsEmpty
PrintList
DeleteList

二、数组实现表——线性表

对表的所有操作都可以通过数组来实现。数组在内存分配上来看是连续储存的。如下图这个大小为9的数组(左边数字为其下标,右边数组是内存地址)。
List | 链表实现表(单向链表)_第2张图片

1、空间缺陷

虽说数组是动态指定的,但是还是需要对表的大小的最大值进行估计。为了防止溢出我们通常会估计得比较大,这样一来会浪费大量的空间。所以在表的大小未知或不确定的情况下,用数组的来实现表是一个比较局限的操作

2、时间缺陷

由于数组的连续储存特性:数组实现使得PrintList和Find操作以线性时间来执行。而在指定位置Insert和Delete花费时间也是线性的,这么看来不是很划算。

由于删除和插入的时间如此昂贵且要求表的大小提前预知,所以我们通常不使用简单数组来实现表这种结构。

三、链表实现表——链式表

链表:物理上非连续、非顺序的储存结构,数据元素之间的顺序是通过每个元素的指针关联的。
链表可以大致分为:单向链表、双向链表(也可以为他们加上循环特性)。

本文都是单向链表的总结,双向链表可以戳链接。

为了避免插入和删除的时间开销,我们允许表可以不连续储存。如下图,链表就很好地实现了这一想法。
在这里插入图片描述


下面给出链表的实现代码,采用分块的形式,在实际使用中其实不一定要固定这样写,怎么方便怎么来。只是各种操作的思想本质都包含在内了,自己总结一套方法(如果觉得我的行也很OK),灵活使用就好。


1、声明链表节点

List | 链表实现表(单向链表)_第3张图片
链表由一系列不必在内存中相连的结构组成,我们称之为链表的节点。每一个节点包至少含有表数据(数据域)、指向后继元的指针,通常称为next指针(指针域)。最后一个单元的next显然应该指向end,即NULL。

typedef struct Node {
    int data;  //储存表中数据
    struct Node* next;  //指向下一个节点的结构体指针
} *NODE, node;

2、创建链表

NODE CreatList() {
    NODE Head = (NODE)malloc(sizeof(node));   //申请一个节点作为表头
    Head->next= NULL;
    return Head;
}

返回值为head(结构体指针)。head通常做为标志节点,实际上不存储数据,仅仅作为表头,方便之后遍历整个表。可以看到,很多对表的操作都需要传入表,但是一般都是传入表头指针。

3、插入操作

可以看到,对链表实行插入操作仅耗费常数时间。且链表的大小是动态变化的。

传入参数为待插入数据x,还有表头Head。

头插法:

将数据插入表首L
List | 链表实现表(单向链表)_第4张图片

void Insert(int x, NODE Head) {
    NODE temp = (NODE)malloc(sizeof(node));   //申请一个待插入节点
    temp->data =x;  //在节点中存入数据
    
    temp->next = Head->next;  //将表头后的节点连在待插入节点后
    Head->next = temp; //将待插入节点连在表头后
}
尾插法

将数据插入表尾

void Insert(int x, NODE Head) {
    NODE temp = (NODE) malloc(sizeof(node));   //申请一个待插入节点
    temp->data = x;  //在节点中存入数据
    temp->next = NULL;
    
    NODE p = Head;  
    //找到表尾
    while (p->next)
        p = p->next;
    
    p->next = temp;  //将待插入节点连入表尾上
}

4、删除操作

删除一个节点q,本质就是把q的前驱元和后继元连上。为了无效的内存访问,一定要考虑到删除数据是否在表内的情况。
List | 链表实现表(单向链表)_第5张图片
传入参数为待删除数据x,还有表头Head。

void Delete(int x, NODE Head) {
    NODE p = Head;

    //找到待删除的元素
    while (p->next && p->next->data != x)
        p = p->next;

    //找到待删除节点,p为其前驱元
    if(p->next) {
        NODE temp = p->next;  //待删除节点
        p->next = temp->next;  //前驱元和后继元连上
        free(temp);  //释放待删除节点,防止内存泄漏
    }
}

5、查找操作

传入参数为待查找数据x,还有表头Head。

NODE Find(int x, NODE Head) {
    NODE p = Head->next;

    //找到待查找的元素
    while (p && p->data != x)
        p = p->next;

    //返回x所在节点
    return p;   //若没有找到x则会返回NULL
}

6、检查是否为空表

int IsEmpty(NODE Head) {
    //如果表头是空则为空表
    return Head->next == NULL;
}

7、打印表

void PrintList(NODE Head) {
    NODE p = Head->next;
    while (p) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

8、删除表

void DeleteList(NODE Head) {
    NODE p = Head;
    while (p) {
        NODE temp = p->next;
        free(p);
        p = temp;
    }
}

四、总结一些细节

按照平常码题经常出现bug的地方总结一下:

1、遍历链表时一定要注意当前的操作节点是否为NULL,对NULL的任何操作都很容易RE(无效内存访问)。

2、对于数据比较多的表,在每次申请节点(即malloc操作后)最好还是判断一下是否申请成功。否则容易造成内存溢出

NODE temp = (NODE)malloc(sizeof(node));   //申请一个待插入节点
if(!temp)  //内存申请失败
    FatalError("Out of space!");

3、养成好的编程习惯,对于无用的表元素,即时释放其内存空间,即free操作



end


欢迎关注个人公众号“鸡翅编程”,平常会把笔记汇总成推送更新~
List | 链表实现表(单向链表)_第6张图片

你可能感兴趣的:(★,数据结构)