【数据结构】【C/C++】链表基础知识,链表的结点定义、初始化、求长度、按序号取元素、按值查询元素、插入、删除、构造的算法实现,各种结构形式的链表(如双循环链表)

一、链表相较于顺序表的优点和缺点

1.优点

1.时间性能差

在顺序表中插入和删除一个元素时,在等概率的情况下,需要移动表中约一半的元素。当表中元素较多时,顺序表的时间性能较低。

2.容易造成空间的浪费

此外,顺序表需要使用连续的内存空间,且空间大小要按照最大的需求分配,可能导致内存空间利用率不高。

2.缺点

1.存储空间的利用率低

每一个结点除了存储要存储的数据之外,还存储了一个指针(如果是双循环链表,则存储了两个指针)。

2.需要查找某一元素的时候,链表不能像顺序表一样直接定位到某一个位置,而是需要去遍历

顺序表由于在内存中连续存储,所以可以快速定位到某一个位置。

二、单链表的存储描述(单链表结构体的定义)

1.结构体定义方法

单链表结点结构由数据域和指针域构成。下面给出代码片段

方法一:

注意:代码中的elementType表示某种数据类型,实际代码中根据需要自行选择。

struct slNode
{
    elementType data;//数据域
    struct slNode * next;//指针域,结构(结点)自身引用
};
typedef struct slNode node, * linkList;//或typedef slNode node, * linkList;

方法二:(将方法一两部分集成描述)

typedef struct slNode
{
    elementType data;//数据域
    struct slNode * next;//指针域,结构(结点)自身引用
}node, * linkList;

上面的描述中使用typedef将结点类型重新命名为node和结点指针类型linkList。以后可以使用类型node来定义结点变量或结点指针变量,也可用linkList直接定义结点指针变量。

2.申请、释放内存方法

这里的链表要求按需分配结点的存储空间,即要求程序在运行期间可以动态地向操作系统申请需要的存储空间,使用完毕后立即释放空间,这样的链表称为“动态链表”。(与之对应的,也有“静态链表”,在Java、python等没有指针的语言中,常采用数组来实现链表,这样的链表称之为静态链表)

C语言中使用malloc()库函数申请空间,使用free()库函数释放空间。

C++使用new操作符申请空间,使用delete操作符释放空间。

new申请结点:

node * p;
p=new node;//动态申请一个结点的内存空间,返回结点指针

delete释放结点:

delete p;

定义了一个结点指针p并申请了结点的内存空间之后,使用"->"符号来引用结点的分量,即p->data;p->next。

3.头指针的定义

一个链表由头指针唯一确定,头指针类型与上面定义的指针p或next指针类型相同,所以头指针可以定义为:node * head。也常用单个字符表示头指针,如node * H或node * L。

4.头指针的意义

加了头结点之后,链表的插入和删除操作只能在头结点之后进行,这使得链表所有位置的插入和删除操作步骤相同。此外,只要申请了头结点,则整个链表在存续期间头指针始终不变。

三、链表的基本运算(初始化、求长度、按序号取元素、按值查询元素、插入、删除)

1.初始化

初始化链表即建立一个不含元素结点的空链表,对于带头结点的单链表来说,就是要编写一个函数,在此函数中申请头结点,头结点的next指针置为空,并将头结点的指针(头指针)返回给主调函数。

方法一:

void initiaList(node *&L)
{
    L=new node;//产生头结点,也可用如下语句产生头结点:L=(node * )malloc(sizeof(node))
    L->next=NULL;//设置后继指针为空
}

注意:这里使用了C++的引用往主调函数回传头结点指针(头指针),不能不使用引用,只把初始化函数参数定义为结点指针,即定义为initiaList(node * L),这样不能正确地回传头指针。

方法二:

void initiaList(node * * pL)
{
    ( * pL)=new node;//pL为结点指针的指针,所以(* pL)为结点指针
        //产生头结点,也可用下面语句产生头结点:( * pL)=(node *)malloc(sizeof(node));
    ( * pL)->next=NULL;//设置后继指针为空
}

方法三(最容易理解):

node * initiaList()
{
    node *p;//声明结点指针变量
    p=new node;//产生一个结点
    p->next=NULL;//节点的next指针置为空
    return p;//返回已申请结点的指针
}

2.求链表长度

求链表L的长度就是求出链表L中元素的个数,而链表中没有存储其长度值,因此需要逐个“数”出其结点个数。“数”结点时用一指针(如p)依次指向每个元素结点(初值置为L->next),p每指到一个结点就作一次计数(因此需要设置一个计数变量len,初值为0),直到搜索到最后,即p移出链表。算法描述如下:

int listLength(node* L)
{
    int len = 0;//遍历开始之前,链表长度当然需要置为0
    node* p = L->next;//p初始指向首元素结点
    while (p != NULL)
    {
        len++;//p指向元素结点,计数+1
        p = p->next;//p移到下一个结点,继续后继结点的计数
    }
    return len;//返回链表长度
}

时间复杂度为O(n)。

3.按序号取元素结点

从首元素结点依次“数”过去即可。另外需要考虑所指结点不存在时所返回的值。

node* getElement(node* L, int i)//i时目标元素结点的序号
{
    node* p = L->next;
    int j = 1;
    while (j != i && p != NULL)//当前结点不是目标结点,并且不为空时就继续搜索
    {
        j++;
        p = p->next;
    }
    return p;//返回结果
}

时间复杂度为O(n)。

4.按值查询元素

设置一个指针,依次指示各元素结点,每指向一个结点就判读一次是否指向了目标元素,若是,返回该结点的指针,若不是,继续搜索直到表尾,若没有找到目标节点,返回空指针(一般这样要求)

node* listLocate(node* L, elementType x)
{
    node* p = L->next;//p初始指向首元素结点
    while (p != NULL && p->data != x)//p指元素结点,又不是目标节点,继续搜索下一目标
    {
        p = p->next;
    }
    return p;
}

时间复杂度为O(n)。

5.插入

bool listInsert(node* L, int i, elementType x)
{
    node* p = L;//p指针指向头节点
    node* S;
    int k = 0;
    while (k != i - 1 && p != NULL)//搜索ei-1结点,并取得指向ei-1的指针p
    {
        p = p->next;//p指向下一结点
        k++;
    }
    if (p == NULL)
    {
        return false;//p为空指针,说明插入位置i无效,返回false
    }
    else
    {
        //此时,k=i-1,p为ei-1结点的指针
        S = new node;//动态申请内存,创建一个新节点,即要插入的结点
        S->data = x;
        S->next = p->next;
        p->next = S;
        return true;//正确插入返回true
    }
}

时间复杂度为O(n)。

6.删除

注意C或C++中用户申请的内存,不用时必须用free()或delete显示地释放,系统不能自动回收这部分内存空间。

bool listDelete(node* L, int i)
{
    node* u;
    node* p = L;//指向头结点
    int k = 0;
    while (k != i - 1 && p != NULL)//搜索ei-1结点
    {
        p = p->next;
        k++;
    }
    if (p == NULL || p->next == NULL)
    {
        return false;//删除位置i超出范围,删除失败,返回false
    }
    else
    {
        //此时p指向ei-1
        u = p->next;//u指向待删除结点ei
        p->next = u->next;//ei-1的next指向ei+1结点,或为空(ei-1为最后结点)
        delete u;//释放ei结点
        return true;//成功删除,返回true
    }
}

7.构造

如果先初始化一个链表,然后反复调用插入结点运算,也可以实现目的,但是每次都要搜索插入位置i,时间性能不理想。因此,要在构造算法中,想办法记住上一次的插入位置。

1.尾插法创建链表

1.结束符控制创建结束
void createListR(node*& L)
{
    elementType x;//保存键盘输入的数据元素值
    node* u, * R;//L为头结点指针(头指针),R为尾结点的指针
    L = new node;//产生头结点,头指针为L
    L->next = NULL;//头结点的指针域为空
    R = L;//设置尾指针,对空链表,头尾指针显然相同
    cout << "请输入元素数据(整数,9999退出):" << endl;//以9999为例
    cin >> x;
    while (x != 9999)
    {
        u = new node;//动态申请内存,产生新结点
        u->data = x;
        u->next = NULL;//新结点的next指针置空
        R->next = u;//新结点链接到表尾
        R = u;//修改尾指针,使指向新的尾结点
        cin >> x;//读入下一个键盘输入数据
    }
}

  时间复杂度为O(n)。

2.结点个数控制创建结束
void createList(node*& L)
{
    int i, n;//n为结点个数,不含头结点
    int x;//x为数据元素值
    node* u, * R;//R为尾结点指针
    L = new node;//产生头结点
    L->next = NULL;//头结点的指针域为空
    R = L;//设置尾指针,对空链表,头尾指针显然相同
    cout << "请输入元素结点个数(整数)" << endl;
    cin >> n;
    cout << "请输入元素数据(整数)" << endl;
    for (i = n; i > 0; i--)
    {
        u = new node;//动态申请内存,产生新结点
        cin >> x;
        u->data = x;//元素数据装入新结点
        u->next = NULL;
        R->next = u;//新结点链接到表尾
        R = u;
    }
}

 时间复杂度为O(n)。

2.头插法

1.结束符控制创建结束
void createListH(node*& L)
{
    node* u;
    elementType x;//存放元素数值
    L = new node;
    L->next = NULL;
    cout << "请输入元素数据(整数,9999退出):" << endl;//以9999为例
    cin >> x;
    while (x != 9999)
    {
        u = new node;//动态申请内存,产生新结点
        u->data = x;
        u->next = L->next;//新结点链接到表头,使成为首元素结点
        L->next = u;
        cin >> x;//读入下一个键盘输入数据
    }
}

时间复杂度为O(n)。 

2.结点个数控制创建结束

类比即可,此处从略

四、其他结构形式的链表

1.单循环链表

将单链表的表尾结点中的后继指针(next)改为指向表头结点,即构成单循环链表。单循环链表要用P==L或P->next==L来判断是否搜索到表尾。

使用单链表要注意不要造成死循环。

2.带尾指针的单循环链表

能够方便地搜索到链表的表头和表尾结点。

3.双链表结构

每个结点同时有后继指针和前驱指针,使得可以方便地访问前驱、后继。

1.双循环链表的初始化

void initialList(dnode*& L)
{
    L=new dnode;
    L->piror=L;
    L->next=L;
}

2.在双循环链表中插入结点

此处省略代码,仅展示步骤:

第一步:搜索插入位置,获取ei结点的指针p

第二步:申请新结点

第三步:新结点向前链接到ei-1

第四步:新结点向后链接到ei

第五步:ei结点前向链接到新结点

第六步:ei-1结点后向链接到新结点

3.在双循环链表中删除结点

此处省略代码,仅展示步骤:

第一步:搜索ei结点,以指针p指向

第二步:ei-1结点的next指向ei+1结点

第三步:ei+1结点的prior指向ei-1结点

第四步:释放结点p(delete操作符或free()函数)

五、没有指针的语言(例如Java、python)中如何实现链表

以数组模拟,以元素下标表示位置,这里不详细展开介绍。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值