zoukankan      html  css  js  c++  java
  • 链表总结

    链表是一种零散的线性数据结构。链表建立、插入、删除、查找、遍历等基本操作。链表的插入删除的时间复杂度为$O(1)$,而查找的时间复杂度为$O(n)$。

    按照组织的方式,链表可以分为单链表,双链表,环形链表。

    单链表的节点只包括数据域和一个指针域,其中指针域指向其后继节点,因此只能单向访问,不能够访问前置节点。双向链表则包括了两个指针域,分别指向其前驱和后继节点。循环链表则是双向链表的一种延伸,其最后一个节点的后继不是指向了nullptr而是指向了第一个节点。(nullptr是C++11标准中的空指针的保留字。)

    一般说链表可以带头指针也可以不带头指针,但是带了头指针则会充分利用nullptr可以直接赋值的特性,使一些操作边界条件处理简化(比如删除节点为第一个节点)。nullptr的这些技巧在二叉树中也会有使用。

    下面以双向带头链表为例来说明链表这一数据结构的基本操作:

    首先需要定义链表的节点:

     1 template<typename T>
     2 struct node
     3 {
     4     T value;
     5     node * next;
     6     node * prev;
     7     node()
     8     {
     9         next = prev = nullptr;
    10     }
    11 };

    这里自行定义了一个默认的构造函数,用来把新建的节点的指针域全部设置为nullptr。这样可以在后续操作时更加简洁。

    (1)链表的建立

    链表的建立非常简单,只需要构造出一个表即可。我这里使用了一个带头链表,因此只需返回头节点的指针即可,因为初始化在构造函数中完成了,因此这里直接返回一个new即可。

    1 template<typename T>
    2 node<T> * CreateList()
    3 {
    4     return new node<T>;
    5 }

    (2)链表的插入

    对于链表的插入有两种处理方法:头插法和尾插法。顾名思义,头插法将新的节点插入在已知链表的第一个节点之前;而尾插法则是要保存一个末尾指针,插入时先将末尾指针所指向的节点的后继设置为新的节点,然后更新末尾指针为新的节点。在这里假设使用new运算符新建的节点具有构造函数能把所有的指针域设为nullptr,因此没有描述哪些指针需要设置为空。如果使用C语言中的malloc()函数或者没有显式给出构造函数去初始化指针域,将未使用的指针域设置为空这一点还是需要注意的。下面给出头插法和尾插法的代码:

    头插法:

     1 template<typename T>
     2 node<T> * InsertAtHead(node<T> * list, T value)
     3 {
     4     node<T> * pnode;
     5     pnode = new node<T>;
     6     pnode->value = value;
     7     pnode->next = list->next;
     8     if (list->next != nullptr)
     9         list->next->prev = pnode;
    10     pnode->prev = list;
    11     list->next = pnode;
    12     return pnode;
    13 }

    尾插法:

     1 template<typename T>
     2 node<T> * InsertAtTail(node<T> * list, T value, node<T> * tail)
     3 {
     4     node<T> * pnode;
     5     pnode = new node;
     6     pnode->next = nullptr;
     7     pnode->value = value;
     8     tail->next = pnode;
     9     return pnode;
    10 }

    个人更倾向于为链表的接口单独提供一个类(C不支持类,可以提供一个结构体,用来单独保存尾插法所需的tail指针。),这样使得代码更为简洁明了。(3)链表的查找

    链表本质上是一个顺序表,因此查找链表只能从头或者从尾顺序查找。和数组的顺序查找较为类似,只要一个个的遍历整个表中的元素,判断其元素是否符合要求的元素。找不到则返回空指针即可。

    下面给出了双向链表的查找:

     1 template<typename T>
     2 node<T> * FindNode(node<T> * list, T value)
     3 {
     4     node<T> * p;
     5     p = list->next;
     6     while (p != nullptr)
     7     {
     8         if (p->value == value)
     9             return p;
    10         p = p->next;
    11     }
    12     return nullptr;
    13 }

    当然这里使用了模板的方法,要求模板的实例化后的类T具有==运算符。

    至于单向链表的查找其实一样,但是如果想要利用查找的结果来进行删除操作,则务必返回前一个节点,这样查找的具体代码又会有所不同。

    (4)链表节点的删除

    删除节点略有麻烦,因为要涉及到前后指针域的修改。前面提及为了简化边界条件的处理,使用了带头的链表。这样只需判断该节点的后继是否为空(是不是最后一个节点)即可,如果非空则将其的前驱设置为被删除节点的前驱。如果不使用带头表,那么也需要判断一下该节点的前驱是否为空(是不是第一个节点)。当然,如果一开始构造了一个带头又带尾的表,那么两个判断都是不需要的。

    代码实现如下:

    1 template<typename T>
    2 void DeleteNode(node<T> * lnode)
    3 {
    4     node<T> * pnode = lnode;
    5     if (lnode->next != nullptr)
    6         lnode->next->prev = lnode->prev;
    7     lnode->prev->next = lnode->next;
    8     delete pnode;
    9 }

    (5)链表的销毁

    在动态数据结构中,为了避免内存泄漏,在使用结束时应当进行销毁。链表的销毁很简单,只需遍历整个表,逐个删除节点即可。删除的时候需要注意保存当前节点的后继,以免delete运算符释放掉当前节点后无法找到其下一个节点。

     1 template<typename T>
     2 void DestoryList(node<T> * list)
     3 {
     4     node<T> * tmp;
     5     while (list != nullptr)
     6     {
     7         tmp = list;
     8         list = list->next;
     9         delete tmp;
    10     }
    11 }

    切记不能使用节点的析构函数实现递归销毁整个表,这样会导致在删除单个节点时把以该节点为头的子表也全部销毁了。

    链表的用途:链表作为基本的数据结构,除了其本身插入删除非常快之外,还可以实现其他的复杂数据结构和算法,比如可以用链表实现栈和队列,可以处理哈希表的冲突,还可以用邻接表来表示图,等等。

    参考:算法导论(第三版),数据结构与算法分析C语言描述(影印版),机械工业出版社。

  • 相关阅读:
    MarkdownPad 2 HTML 渲染错误解决办法
    ubuntu 修改用户名和密码
    在浏览器输入http://127.0.0.1/phpmyadmin,出现not found界面
    MySQL出现Incorrect integer value: '' for column 'id' at row 1解决方法
    Ubuntu中查找文件
    Python+Selenium--控制浏览器控制条
    关于Python+selenium 定位浏览器弹窗元素
    python中安装request模块
    Python+Selenium--cookie处理
    JS控制HTML元素的显示和隐藏
  • 原文地址:https://www.cnblogs.com/ggggg63/p/6612737.html
Copyright © 2011-2022 走看看