数据结构之链表

一、定义

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成

每个结点包括两个部分:

1、存储数据元素的数据域,
2、下一个结点地址的指针域。

相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。

链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。

**链表有很多种不同的类型:

单向链表
双向链表
循环链表

链表可以在多种编程语言中实现。程序语言或面向对象语言,如C,C++和Java依靠易变工具来生成链表。

循环链表是与单链表一样,是一种链式的存储结构,所不同的是,循环链表的最后一个结点的指针是指向该循环链表的第一个结点或者表头结点,从而构成一个环形的链。

循环链表的运算与单链表的运算基本一致。

所不同的有以下几点:

1、在建立一个循环链表时,必须使其最后一个结点的指针指向表头结点,而不是象单链表那样置为NULL。此种情况还使用于在最后一个结点后插入一个新的结点。
2、在判断是否到表尾时,是判断该结点链域的值是否是表头结点,当链域值等于表头指针时,说明已到表尾。而非象单链表那样判断链域值是否为NULL。

双向链表

双向链表其实是单链表的改进。

当我们对单链表进行操作时,有时你要对某个结点的直接前驱进行操作时,又必须从表头开始查找。这是由单链表结点的结构所限制的。因为单链表每个结点只有一个存储直接后继结点地址的链域,那么能不能定义一个既有存储直接后继结点地址的链域,又有存储直接前驱结点地址的链域的这样一个双链域结点结构呢?这就是双向链表。

在双向链表中,结点除含有数据域外,还有两个链域,一个存储直接后继结点地址,一般称之为右链域;一个存储直接前驱结点地址,一般称之为左链域

链表的提出主要在于顺序存储中的插入和删除的时间复杂度是线性时间的,而链表的操作则可以是常数时间的复杂度。对于链表的插入与删除操作,个人做了一点总结,适用于各种链表如下:

插入操作处理顺序:中间节点的逻辑,后节点逻辑,前节点逻辑。
按照这个顺序处理可以完成任何链表的插入操作。

删除操作的处理顺序:前节点逻辑,后节点逻辑,中间节点逻辑。
按照此顺序可以处理任何链表的删除操作。如果不存在其中的某个节点略过即可。

上面的总结,大家可以看到一个现象,就是插入的顺序和删除的顺序恰好是相反的,很有意思!

二、单链表的实现

1、建立单链表

动态地建立单链表的常用方法有如下两种:

(1) 头插法建表

① 算法思路

从一个空表开始,重复读入数据,生成新结点,将读入数据存放在新结点的数据域中,然后将新结点插入到当前链表的表头上,直到读入结束标志为止。

这里写图片描述

注意:
 该方法生成的链表的结点次序与输入顺序相反
 插图不含头结点,仅有头指针,假设图中head代表头结点,下面代码采用的是带有头结点的实现

② 具体算法实现

void List::CreatListF(int i)//头插法创建链表
{

    Node *s = new Node;
    s->data = i;
    s->next = NULL;

    if (head->next == NULL)
        head->next = s;

    else
    {
        s->next = head->next;
        head->next = s;
    }

}

(2) 尾插法建表

① 算法思路

从一个空表开始,重复读入数据,生成新结点,将读入数据存放在新结点的数据域中,然后将新结点插入到当前链表的表尾上,直到读入结束标志为止。

这里写图片描述
注意1:

 ⒈采用尾插法建表,生成的链表中结点的次序和输入顺序一致
  ⒉必须增加一个尾指针r,使其始终指向当前链表的尾结点

注意2:

  ⒈开始结点插入的特殊处理
由于开始结点的位置是存放在头指针(指针变量)中,而其余结点的位置是在其前趋结点的指针域中,插入开始结点时要将头指针指向开始结点。
 ⒉空表和非空表的不同处理
若读入的第一个字符就是结束标志符,则链表head是空表,尾指针r亦为空,结点*r不存在;否则链表head非空,最后一个尾结点*r是终端结点,应将其指针域置空。

(3) 尾插法建带头结点的单链表

①头结点及作用

头结点是在链表的开始结点之前附加一个结点。

它具有两个优点:
 ⒈由于开始结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作就和在表的其它位置上操作一致,无须进行特殊处理;
 ⒉无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域空),因此空表和非空表的处理也就统一了。

②带头结点的单链表
这里写图片描述

注意:   
头结点数据域的阴影表示该部分不存储信息。在有的应用中可用于存放表长等附加信息。

③尾插法建带头结点链表算法

void List::CreatListR(int i)//尾插法创建链表
{
    Node *s = new Node;
    s->data = i;
    s->next = NULL;

    tail->next = s;
    tail = s;
}

注意:

上述算法里,动态申请新结点空间时未加错误处理,这对申请空间极少的程序而言不会出问题。但在实用程序里,尤其是对空间需求较大的程序,凡是涉及动态申请空间,一定要加入错误处理以防系统无空间可供分配。

(4) 算法时间复杂度
 以上三个算法的时间复杂度均为0(n)。

2.单链表的查找运算

(1)按序号查找

① 链表不是随机存取结构

在链表中,即使知道被访问结点的序号i,也不能像顺序表中那样直接按序号i访问结点,而只能从链表的头指针出发,顺链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构。

② 查找的思想方法

计数器j置为0后,扫描指针p指针从链表的头结点开始顺着链扫描。当p扫描下一个结点时,计数器j相应地加1。当j=i时,指针p所指的结点就是要找的第i个结点。而当p指针指为null且j≠i时,则表示找不到第i个结点。

注意:

 头结点可看做是第0个结点。

③具体算法实现

Node *List::GetNode(int i)//获取第i个节点位置
{
    //在带头结点的单链表head中查找第i个结点,若找到(0≤i≤n),
    //则返回该结点的存储位置,否则返回NULL。

    Node *p = head;//从头结点开始扫描
    int j = 0;

    while (p->next&&j < i)
    {//顺指针向后扫描,直到p->next为NULL或i=j为止

        p = p->next;
        j++;
    }

    if (i == j)//找到了第i个结点
        return p;
    else
        return NULL;//当i<0或i>0时,找不到第i个结点

}

④算法分析
 算法中,while语句的终止条件是搜索到表尾或者满足j≥i,其频度最多为i,它和被寻找的位置有关。在等概率假设下,平均时间复杂度为:
 这里写图片描述
(2) 按值查找

①思想方法

从开始结点出发,顺着链逐个将结点的值和给定值key作比较,若有结点的值与key相等,则返回首次找到的其值为key的结点的存储位置;否则返回NULL。

②具体算法实现

int List::GetKeyNode(int key)//获取第节点数据域为key的位置,如果有两个key,可以返回两个位置
{
    Node *p = head;
    int j = 0;
    while (p)
    {
        if (p->data == key)
            return j;
        p = p->next;
        j++;
    }

}

③算法分析

该算法的执行时间亦与输入实例中key的取值相关,其平均时间复杂度分析类似于按序号查找,为O(n)。

3.插入运算

(1)思想方法

插入运算是将值为x的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间。
 
具体步骤:

(1)找到ai-1存储位置p
(2)生成一个数据域为x的新结点*s
(3)令结点*p的指针域指向新结点
(4)新结点的指针域指向结点ai。

这里写图片描述
(2)具体算法实现

void List::Insert(int i, int x)//在第i个位置插入
{
    Node *p = GetNode(i - 1);//找到第i-1个节点

    if (p == NULL)
        std::cout << "position error" << std::endl;

    Node *newNode = new Node;
    newNode->data = x;
    newNode->next = p->next;
    p->next = newNode;

}

(3)算法分析
 算法的时间主要耗费在查找操作GetNode上,故时间复杂度亦为O(n)。

4.删除运算

(1)思想方法

删除运算是将表的第i个结点删去。

具体步骤:
(1)找到ai-1的存储位置p(因为在单链表中结点ai的存储地址是在其直接前趋结点ai-1的指针域next中)
(2)令p->next指向ai的直接后继结点(即把ai从链上摘下)
(3)释放结点ai的空间,将其归还给”存储池”。

这里写图片描述
(2)具体算法实现

int List::Delete(int i)//删除第i个位置上的节点
{   
    Node *p = GetNode(i - 1);//找到第i-1个节点

    if (p == NULL || p->next == NULL)
        std::cout << "position error" << std::endl;

    Node *deletedNode = p->next;
    p->next = deletedNode->next;

    int val = deletedNode->data;

    delete deletedNode;

    return val;
}

注意:

设单链表的长度为n,则删去第i个结点仅当1≤i≤n时是合法的。

当i=n+1时,虽然被删结点不存在,但其前趋结点却存在,它是终端结点。因此被删结点的直接前趋*p存在并不意味着被删结点就一定存在,仅当*p存在(即p!=NULL)且*p不是终端结点(即p->next!=NULL)时,才能确定被删结点存在。

(3)算法分析

算法的时间复杂度也是O(n)。
链表上实现的插入和删除运算,无须移动结点,仅需修改指针。

5.整表删除运算

void List::ClearList()
{

    Node *p = head->next;//指向第一个结点
    Node *q;

    while (p)
    {
        q = p->next;
        delete p;
        p = q;
    }
    head->next = NULL;//头结点指针域必须为空

    //下面此方法较简单,一次删除一个节点
    //for (int i = 1; i <= Length(); ++i)
    //  Delete(i);
    //head->next = NULL;//头结点指针域必须为空
}

6.所有操作的完整代码实现:

//头文件

#ifndef LIST_H
#define LIST_H
#include <iostream>

struct Node
{
    int data;
    Node *next;
};

class List
{
public:
    List();
    ~List(){}

    void CreatListF(int i); //头插法创建链表
    void CreatListR(int i);//尾插法创建链表
    void ClearList();


    Node *GetNode(int i);//获取第i个节点位置
    int GetKeyNode(int key);

    void Insert(int i, int x);  //在第i个节点后边插入
    int Delete(int i);  //删除第i个节点
    void Display();  //显示各个节点的关键字data
    int Length();//返回链表中节点个数

private:
    Node *head;
    Node *tail;
};


#endif // !LIST_H



//实现文件

#include "list.h"

List::List()
{
    head = tail = new Node; 
    head->next = NULL;
}

void List::CreatListF(int i)//头插法创建链表
{

    Node *s = new Node;
    s->data = i;
    s->next = NULL;

    if (head->next == NULL)
        head->next = s;

    else
    {
        s->next = head->next;
        head->next = s;
    }

}

void List::CreatListR(int i)//尾插法创建链表
{
    Node *s = new Node;
    s->data = i;
    s->next = NULL;

    tail->next = s;
    tail = s;
}

void List::ClearList()
{

    Node *p = head->next;//指向第一个结点
    Node *q;

    while (p)
    {
        q = p->next;
        delete p;
        p = q;
    }
    head->next = NULL;//头结点指针域必须为空

    //下面此方法较简单,一次删除一个节点
    //for (int i = 1; i <= Length(); ++i)
    //  Delete(i);
    //head->next = NULL;//头结点指针域必须为空
}

Node *List::GetNode(int i)//获取第i个节点位置
{
    //在带头结点的单链表head中查找第i个结点,若找到(0≤i≤n),
    //则返回该结点的存储位置,否则返回NULL。

    Node *p = head;//从头结点开始扫描
    int j = 0;

    while (p->next&&j < i)
    {//顺指针向后扫描,直到p->next为NULL或i=j为止

        p = p->next;
        j++;
    }

    if (i == j)//找到了第i个结点
        return p;
    else
        return NULL;//当i<0或i>0时,找不到第i个结点


}

int List::GetKeyNode(int key)//获取第节点数据域为key的位置,如果有两个key,可以返回两个位置
{
    Node *p = head;
    int j = 0;
    while (p)
    {
        if (p->data == key)
            return j;
        p = p->next;
        j++;
    }

}

void List::Insert(int i, int x)//在第i个位置插入
{
    Node *p = GetNode(i - 1);//找到第i-1个节点

    if (p == NULL)
        std::cout << "position error" << std::endl;

    Node *newNode = new Node;
    newNode->data = x;
    newNode->next = p->next;
    p->next = newNode;

}

int List::Delete(int i)//删除第i个位置上的节点
{   
    Node *p = GetNode(i - 1);//找到第i-1个节点

    if (p == NULL || p->next == NULL)
        std::cout << "position error" << std::endl;

    Node *deletedNode = p->next;
    p->next = deletedNode->next;

    int val = deletedNode->data;

    delete deletedNode;

    return val;
}

void List::Display()//显示各节点的data
{
    Node *p = head->next;//第一个节点
    while (p)
    {
        std::cout << p->data << " ";
        p = p->next;
    }
}

int List::Length()//返回链表中节点个数
{
    Node *p = head->next;
    int j = 0;
    while (p)
    {
        j++;
        p = p->next;
    }

    return j;
}


//测试文件

#include "list.h"

using namespace std;

int main()
{
    List ls;
    int key = 7;

    for (int i = 0; i < 10; ++i)
        ls.CreatListF(i);

    cout << "链表中的元素是:";
    ls.Display();
    cout << endl;

    cout << "链表中节点个数:" << ls.Length() << endl;

    cout << "链表中关键字为key的节点序号是:" << ls.GetKeyNode(key) << endl;

    for (int i = 0; i < 5; ++i)
        ls.CreatListR(i);

    cout << "链表中的元素是:";
    ls.Display();
    cout << endl;

    cout << "链表中节点个数:" << ls.Length() << endl;

    cout << "在第2个位置插入节点:" << endl;
    ls.Insert(2, 100);

    cout << "链表中的元素是:";
    ls.Display();
    cout << endl;

    cout << "链表中节点个数:" << ls.Length() << endl;

    cout << "删除第2个位置节点是:" << ls.Delete(2) << endl;


    cout << "链表中的元素是:";
    ls.Display();
    cout << endl;

    cout << "链表中节点个数:" << ls.Length() << endl;



    ls.ClearList();
    cout << "链表中节点个数:" << ls.Length() << endl;

    system("pause");
    return 0;
}

版权声明:本文为博主原创文章,未经博主允许不得转载。

转载于:https://www.cnblogs.com/yangquanhui/p/4937458.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值