IT狗工作室C语言C语言&嵌入式

第2篇 C++ 数据结构-链表概述

2020-01-02  本文已影响0人  铁甲万能狗

本篇我们会讨论单向链表,在所有线性存储结构当中,链表是最常用且非常重要的数据结构,因为链表实现队列(Queue)以及环形队列(Circle Queue),栈(Stack)的最佳选择,在许多app中,单链表应用随处可见。例如很多即时通讯工具的好友列表,电话中的通讯录,以及媒体播放器的播放列表都广泛地用到了链表。

单向链表(Single-LinkedList),由一个或多个节点(Node)组成,每个结点承载着特定数据类型的对象。


链表示意图

结点(Node)

ArrayList vs LinkedList

我们用一个形象生动的例子,来比较一下ArrayList和LinkedList的差别

那么我们来看看ArrayList与LinkedList的各种操作下的时间复杂度对比,这个表并没有考虑到最差的情况。这表面上看,LinkedList似乎跟ArrayList没什么优势。LinkList在中间位置和末尾插入的时间复杂度之所以为O(n),是因为每次在插入元素之前必须发生遍历操作找到插入元素对应的位置。

性能比较

如果你对标准库vector或我前一篇文章ArrayList的内部堆内存管理有所了解的话,应该知道ArrayList在添加或删除达到一个指定比例值时,内存可能需要扩容/收缩(简称resize).

假设ArrayList的元素类型为T,原有内存数量为n,resize的内存数量为m=malloc(sizeof(T)*n)的内存空间重分配的时间复杂度为O(m)这个malloc操作属于操作系统底层的,另外ArrayList本身n个元素的深度拷贝到新堆内存空间的一系列过程,这个时间复杂度就为O(n)那么这种最差的情况下

那么,ArrayList在插入/删除操作最差的情况,确切是O(m)+O(n)

LinkedList相比之下就没有这些昂贵的操作,LinkedList只会对插入/删除的元素的一次性malloc或free操作,无需额外的连续内存重分配。所以LinkedList在插入或删除方面有着明显的性能优势。

需要注意的是遍历操作下标访问是两个绝然不同的概念

for(auto i=0;i<arr.size;i++){....}
//或
foreach(item in arr){...}
//或
size_t i=0;
while(i<arr.size){...}
//或
while(arr.next()){...}

链表类接口的基本定义

以前说过用C++实现数据结构合适,写类接口对比C代码,C++在语法上更优雅和逻辑清晰,并且C++具有访问级别的限制,使得外部调用代码无法随意修改链表中的数据。该系列的文章,我会用到C++来实现该单向链表。

首先LinkedList由一个节点类(Node)和一个链表类(LinkedList)构成。

Node类接口

这里需要注意的是我们定义的同时,我们需要类内部声明一个LinkedList接口的友元函数,以便LinkedList类接口的成员函数能够访问Node类接口的私有数据成员。这是设计模式中典型的组合模式,在C++中普遍存在。

#include <iostream>
#include <stdlib.h>

template <class T>
class LinkedList;

template <class T>
class Node
{
private:
    Node *d_next; //指向下一个节点
    T d_data;

public:
    Node();
    //自定义构造函数
    Node(T);
    //返回节点上的元素实体
    const T &elem();

    //返回当前节点的下一个节点的地址
    Node<T> *next();

    //析构函数
    ~Node();
    //LinkList的友元函数
    friend class LinkedList<T>;

    template <typename R>
    friend std::ostream &operator<<(std::ostream &, const Node<R> &);
};

LinkedList类接口

template <class T>
class LinkedList
{
private:
    //头指针
    Node<T> *d_head;
    //中间节点
    Node<T> *d_mid;
    //未指针
    Node<T> *d_last;
    //链表尺寸
    size_t d_size;

    //查找元素所在位置
    Node<T> *_search(const T &val);
    //二分查找法
    size_t binary_search(size_t s, size_t e, size_t k);

public:
    //默认构造函数(空链表)
    LinkedList();

    LinkedList(size_t);
    //拷贝构造函数
    LinkedList(const LinkedList &list);
    //移动构造函数
    LinkedList(LinkedList &&otl);
    //析构函数
    ~LinkedList();
    //末端插入元素
    void push_back(T);
    //在头部插入元素
    void push_head(T);

    //获取头部元素
    T front();
    // 获取末端元素
    T last();

    //颠倒链表顺序
    void reverse();

    //查找元素的值,删除该元素
    void remove(const T &value);
    //在指定位置,插入元素
    void insert(size_t idx, T elem);

    //迭代接口:返回下一个节点
    // T next();

    //公开的查找节点函数
    Node<T> *search(const T &val);

    Node<T> *search_v2(size_t);

    //清空整个链表
    void clear();

    //打印链表
    template <class R>
    friend std::ostream &operator<<(std::ostream &, const LinkedList<R> &);

    //索引访问操作符号
    T operator[](size_t);

    //获取尺寸
    size_t size()
    {
        return d_size;
    }
    //获取中间节点
    Node<T> *mid_node();
    //获取中间节点
    Node<T> *mid_node(size_t &);
};

小结

本篇主要是对链表一个概念,以及和ArrayList做了一些性能上的对比,可以知道,在写入/删除元素操作方面具有其他线性表结构代替的性能优势。似乎单项链表本质上来说是不支持下标索引访问,只能通过每次O(n)的遍历来模拟下标索引的访问操作,因此这方面不如ArrayList和标准库的vector,后面的相关随笔会谈到这方面。很晚了,睡觉去~~!!

上一篇 下一篇

猜你喜欢

热点阅读