2.3 线性表的链式存储结构(链表)

基本概念和特点

链表的定义

-线性表的链式存储结构称之为链表(linked list)。链表包括两部分构成:数据域和指针域。数据域保存数据元素,指针域描述数据元素的逻辑关系。
- 链表通常使用带头结点的表示。指向头结点的指针称之为头指针(head pointer),指向最后一个结点(也就是尾结点)的指针称之为尾指针(tail pointer)。
- 链表不具备随机存取特性。
- 存储密度(storage density)是指数据的体积占结点总体积的比。链表的存储密度总小于1(因为有指针域)。

链表的类型及其特点

  • 链表根据指针域的指向,分为单链表、双链表。单链表只能通过前驱找到后继,不能通过后继找到前驱。双链表使用双向指针域,方便了反向查找,是一种时空权衡的做法。
  • 根据尾结点和头结点能否构成联系,分为普通链表和循环链表。

单链表的表示和基本操作

单链表的表示

因为链表本身依然是属于线性结构,也是线性表,因此类接口与线性表无异。线性表具备的方法都应该满足(但是不意味着能够以最高效率实现)。
下面是使用C++语言的类描述。开始定义两个宏来表示初始化是使用头插法还是尾插法。
定义结点采用了类的嵌套,因为要保证结点的数据类型和主类一致,使用嵌套类能够避免很多数据类型不兼容的情况。STL就是这么做的。除了这种方法能够实现数据结构的泛型,对于C,因为没有模板,而数据结构通常作为底层的库,必须具备泛型特性,因此通常直接在内存中根据大小存放数据,实际使用时,再将其指针进行强制类型转换为需要的指针。例如下面给出的php 7的链表结点实现(使用变长结构),就是基于这样的考虑。

/* php v7.1.4 zend_llist.h */
typedef struct _zend_llist_element {
    struct _zend_llist_element *next;
    struct _zend_llist_element *prev;
    char data[1]; /* Needs to always be last in the struct */
} zend_llist_element;

链表的终止通常用宏NULL来表示,对于C++11,可以使用关键字nullptr来实现空指针。下面的代码都是使用了这一特性的,对于不支持C++11的编译器,可以使用宏#define nullptr NULL来使用下面的代码。
我们需要维护其头指针来标识整个链表,因此设置了_head私有成员变量。_length是为了简化求长度的操作,这样就不必在使用length()方法时,遍历整个表了。只需要额外的4字节开销。这种方法的坏处在于,不能够在类外随便操作链表结点(因为无法更新_length的值)。不过此处的链表依然作为线性表的表示(而算一个玩具),所以并无大碍。

//定义构造函数采用头插法还是尾插法
#define INIT_INSERT_HEAD 1
#define INIT_INSERT_TAIL 0
/**
* 链表类
*/
template<typename _Ty>
class SinglyLList
{
public:
    /**
    * 链表结点结构体
    */
    struct SinglyLListNode{
        _Ty data;   //数据域
        SinglyLList<_Ty>::SinglyLListNode * next;   //指针域
        SinglyLListNode(){
            next = nullptr;
        }
    };
private:
    SinglyLList<_Ty>::SinglyLListNode * _head;  //链表的头指针
    int _length;     //链表的长度(元素个数)
public:
    /**
    * 构造函数,创建一个线性表,并用指定的数据填充线性表。
    * @param _Ty init_data[] 初始输入数据数组
    * @param int n 初始数据数组长度
    * @param int mode 采用的初始化插入方法,默认头插法
    */
    SinglyLList(_Ty init_data[], int n, int mode = INIT_INSERT_HEAD);
    /**
    * 默认构造函数,创建一个空的线性表
    */
    SinglyLList();
    /**
    * 析构函数,销毁线性表
    */
    ~SinglyLList();
    /**
    * 判断当前线性表是否为空
    * @return bool 表空返回true,否则false
    */
    bool empty();
    /**
    * 返回当前线性表的长度
    * @return int 线性表的实际长度
    */
    int length();
    /**
    * 返回线性表中指定位置的元素
    * @param int i 序号
    * @return _Ty 返回元素的值
    */
    _Ty & at(int i);
    _Ty & operator[](int i);
    /**
    * 查找线性表中指定值的元素的位置
    * @param _Ty value 需要查找的元素的值
    * @return int 返回该元素的位置,0为未找到
    */
    int find(_Ty value);
    /**
    * 将指定元素插入指定位置
    * @param int i 待插入元素的位置
    * @param _Ty value 待插入元素的值
    * @return bool 操作成功返回true,否则false
    */
    bool insert(int i, _Ty value);
    /**
    * @param int i 需要删除的元素的位置
    * @return bool 操作成功返回true,否则false
    */
    bool remove(int i);
};

单链表的构造函数

构造函数根据已有数据创建整个链表。这里使用了mode参数决定采用尾插法还是头插法。头插法关注的是_head指针,而尾插法关注的是尾指针,因此维护一个tail是必要的。单链表的插入,先链接当前结点的next,将其指向下一个结点,然后将原来的前驱结点的next设置为当前插入的结点。后面的链表插入也是如此方法。

template<typename _Ty>
SinglyLList<_Ty>::SinglyLList(_Ty init_data[], int n, int mode){
    assert(init_data && n >= 0);
    //需要#include<cassert>
    SinglyLListNode * tail;
    SinglyLListNode * newnode;
    tail = _head = new SinglyLListNode;
    //判断输入数据是否为空
    if(n){
        for(int i = 0; i < n; i++){
            newnode = new SinglyLListNode;
            newnode->data = init_data[i];
            //判断使用头插法还是尾插法
            if(mode == INIT_INSERT_HEAD){
                newnode->next = _head->next;
                _head->next = newnode;
            }else{
                tail->next = newnode;
                tail = newnode;
            }
        }
    }
    //如果SinglyLListNode不具备构造函数,下面的代码是必须的
    //if(mode == INIT_INSERT_TAIL)
    //    tail->next = nullptr;
    _length = n;
}

无参数的构造函数用于创建一个空表,只需要将_length设置为零,创建一个头结点就可以了

template<typename _Ty>
SinglyLList<_Ty>::SinglyLList(){
    _head = new SinglyLListNode;
    _length = 0;
}

析构函数

析构函数用来销毁整个链表,并释放内存空间。主要的方法就是遍历整个表,逐个销毁结点。需要注意的是,不能将指针弄丢了,否则会造成内存泄漏。在具体操作过程中,就是需要将当前结点的next先保存,而后才能释放当前结点。另外,不能忘记释放头结点。

template<typename _Ty>
SinglyLList<_Ty>::~SinglyLList(){
    SinglyLListNode * tnode, * nnode = _head;
    while(nnode != nullptr){
        tnode = nnode->next;
        delete nnode;
        nnode = tnode;
    }
}

判断链表是否是空表

直接判断_length域就可以了。判断_head->next是否为nullptr也是可以的。

template<typename _Ty>
bool SinglyLList<_Ty>::empty(){
    return _length == 0;
}

得到链表的长度

考虑设置_length域的意义,直接返回_length域就可以了,否则需要遍历整个链表。

template<typename _Ty>
int SinglyLList<_Ty>::length(){
    return _length;
}

根据逻辑序号得到元素

链式存储结构不具备直接得到序号的能力,因此需要在遍历过程中自行维护一个计数器count,表示当前结点的序号。注意2.1节线性表的定义中,逻辑序从1开始,因此可以假定没有存储实际数据的头指针的逻辑序为0,以简化操作。实际上从第一个结点以1计算下标,也是正确的。

template<typename _Ty>
_Ty & SinglyLList<_Ty>::at(int i){
    assert(i > 0 && i <= _length);
    int count = 0;
    SinglyLListNode * pnode = _head;
    while(count != i){
        pnode = pnode->next;
        count++;
    }
    return pnode->data;
}

为了使逻辑序更清晰,像顺序表一样也重载operator[]

template<typename _Ty>
_Ty & SinglyLList<_Ty>::operator[](int i){
    return this->at(i);
}

查找指定元素的逻辑序

查找指定元素,一样需要维护一个计数器。一旦找到,则返回其逻辑序,反之,返回0。对于链表,实际上返回逻辑序,并无太大用途,单链表更倾向返回一个当前结点的前驱,以便于进行插入和删除操作。后面的改进会提到这一点,以及如何利用这一点提高某些算法的效率。

template<typename _Ty>
int SinglyLList<_Ty>::find(_Ty value){
    int count = 1;
    SinglyLListNode * pnode = _head->next;
    while(pnode != nullptr){
        if(pnode->data == value)
            return count;
        pnode = pnode->next;
        count++;
    }
    return 
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值