链表与list

1. 链表实现

特点: 每一个节点都是在堆内存上独立 new 出来的,节点内存不连续。即逻辑地址连续,而物理地址不连续。

优点

  • 内存利用率高,不需要大块连续内存
  • 插入和删除节点不需要移动其它节点,时间复杂度 O(1)
  • 不需要专门进行扩容操作

缺点

  • 内存占用量大,每一个节点多出存放地址的空间
  • 节点内存不连续,无法进行内存的随机访问
  • 链表的搜索效率不高,只能从头节点开始逐结点遍历

常见的链表形式包括:单链表、双向链表、环状链表

1.1 单链表

单链表的定义:每一个节点只能找到下一个节点,而无法找到上一个节点,如下图所示:

特点

  • 每一个节点除了数据域,还有一个 next 指针域指向下一个节点(存储了下一个节点的地址),但是无法回退到前一个节点
  • 末尾节点的指针域是 NULL

如下代码为单链表的实现:

// 链表节点定义
struct Node {
    Node(int val = 0)
        : data_(val)
        , next_(nullptr) { }
    int   data_;
    Node* next_;
};

class SList {
public:
    SList();
    ~SList();

public:
    // 尾插法
    void push_back(int val);
    // 头插法
    void push_front(int val);
    // 删除第一个值为 val 节点
    void remove(int val);
    // 删除所有值为 val 的节点
    void removeAll(int val);
    // 搜索是否存在值为 val 的元素
    bool find(int val);
    // 打印
    void print() const;

private:
    Node* head_; // 链表头节点
};

SList::SList()
    : head_(new Node) {
}

SList::~SList() {
    while (head_->next_ != nullptr) {
        Node* node   = head_->next_;
        head_->next_ = node->next_;
        delete node;
    }
    delete head_;
    head_ = nullptr;
}

void SList::push_back(int val) {
    Node* cur = head_;
    while (cur->next_ != nullptr) {
        cur = cur->next_;
    }
    cur->next_ = new Node(val);
}

void SList::push_front(int val) {
    Node* node   = new Node(val);
    node->next_  = head_->next_;
    head_->next_ = node;
}

void SList::remove(int val) {
    Node* cur = head_;
    while (cur->next_ != nullptr) {
        if (cur->next_->data_ == val) {
            Node* node = cur->next_;
            cur->next_ = node->next_;
            delete node;
            break;
        }
        cur = cur->next_;
    }
}

void SList::removeAll(int val) {
    Node* cur = head_;
    while (cur && cur->next_ != nullptr) {
        if (cur->next_->data_ == val) {
            Node* node = cur->next_;
            cur->next_ = node->next_;
            delete node;
        } else {
            cur = cur->next_;
        }
    }
}

bool SList::find(int val) {
    Node* cur = head_->next_;
    while (cur) {
        if (cur->data_ == val) {
            return true;
        }
        cur = cur->next_;
    }
    return false;
}

void SList::print() const {
    Node* cur = head_->next_;
    while (cur != nullptr) {
        cout << cur->data_ << "->";
        cur = cur->next_;
    }
    cout << endl;
}

1.2 单向循环链表

特点

  • 每一个节点除了数据域,还有一个 next 指针域指向下一个节点(存储了下一个节点的地址)
  • 末尾节点的指针域指向了头节点

其代码的实现如下:

class CircleLink {
public:
    CircleLink();
    ~CircleLink();
  
public:
    // 尾插法
    void push_back(int val);
    // 头插法
    void push_front(int val);
    // 删除第一个值为 val 的节点
    void remove(int val);
    // 查询是否存在值为 val 的节点
    bool find(int val);
    // 打印
    void print() const;
  
private:
    struct Node {
        Node(int data = 0)
            : data_(data)
            , next_(nullptr) { }
        int   data_;
        Node* next_;
    };
  
    Node* head_;
    Node* tail_;
};

CircleLink::CircleLink()
    : head_(new Node()) {
    tail_->next_ = head_;
    head_->next_ = head_;
}
  
CircleLink::~CircleLink() {
    Node* cur = head_->next_;
    while (cur != head_) {
        head_->next_ = cur->next_;
        delete cur;
        cur = head_->next_;
    }
    delete head_;
}
  
void CircleLink::push_back(int val) {
    Node* node   = new Node(val);
    node->next_  = tail_->next_;
    tail_->next_ = node;
    tail_        = node;
    if (head_->next_ == head_) {
        head_->next_ = node;
    }
}
  
void CircleLink::push_front(int val) {
    Node* node   = new Node(val);
    node->next_  = head_->next_;
    head_->next_ = node;
    if (node->next_ == head_) {
        tail_ = node;
    }
}
  
void CircleLink::remove(int val) {
    if (head_->next_ == head_) return;
    Node* cur = head_;
    while (cur->next_ != tail_) {
        if (cur->next_->data_ == val) {
            Node* nxt  = cur->next_;
            cur->next_ = nxt->next_;
            delete nxt;
            return;
        }
        cur = cur->next_;
    }
    if (tail_->data_ == val) {
        cur->next_ = tail_->next_;
        delete tail_;
        tail_ = cur;
    }
}
  
bool CircleLink::find(int val) {
    Node* cur = head_->next_;
    while (cur != head_) {
        if (cur->data_ == val)
            return true;
        cur = cur->next_;
    }
    return false;
}
  
void CircleLink::print() const {
    Node* cur = head_->next_;
    while (cur != head_) {
        cout << cur->data_ << "->";
        cur = cur->next_;
    }
    cout << endl;
}

1.3 双向链表

特点

  • 每一个节点除了数据域,还有 next 指针域指向下一个节点,prev 指针域指向前一个节点
  • 头节点的 prev 是 NULL,末尾节点的 next 是 NULL

其代码实现如下:

class DoubleLink {
public:
    DoubleLink();
    ~DoubleLink();
  
    // 头插法
    void push_front(int val);
    // 尾插法
    void push_back(int val);
    // 删除
    void remove(int val);
    // 查找
    bool find(int val);
    // 打印
    void print() const;
  
private:
    // 节点类型
    struct Node {
        Node(int data = 0)
            : data_(data)
            , next_(nullptr)
            , prev_(nullptr) { }
        int   data_;
        Node* next_;
        Node* prev_;
    };
  
    Node* head_;
};

DoubleLink::DoubleLink()
    : head_(new Node()) {
}

DoubleLink::~DoubleLink() {
    Node* cur = head_;
    while (cur) {
        head_ = head_->next_;
        delete cur;
        cur = head_;
    }
}

void DoubleLink::push_back(int val) {
    Node* cur = head_;
    while (cur->next_ != nullptr) {
        cur = cur->next_;
    }
    Node* node  = new Node(val);
    cur->next_  = node;
    node->prev_ = cur;
    node->next_ = nullptr;
}
  
void DoubleLink::push_front(int val) {
    Node* node   = new Node(val);
    Node* nxt    = head_->next_;
    node->next_  = nxt;
    node->prev_  = head_;
    nxt->prev_   = node;
    head_->next_ = node;
}
  
void DoubleLink::remove(int val) {
    Node* cur = head_->next_;
    while (cur) {
        if (cur->data_ == val) {
            cur->prev_->next_ = cur->next_;
            if (cur->next_) {
                cur->next_->prev_ = cur->prev_;
            }
            delete cur;
            break;
        }
        cur = cur->next_;
    }
}
  
bool DoubleLink::find(int val) {
    Node* cur = head_->next_;
    while (cur) {
        if (cur->data_ == val) {
            return true;
        }
        cur = cur->next_;
    }
    return false;
}

void DoubleLink::print() const {
    Node* cur = head_->next_;
    while (cur) {
        cout << cur->data_ << "->";
        cur = cur->next_;
    }
    cout << endl;
}

1.4 双向循环链表

特点

  • 每一个节点除了数据域,还有 next 指针域指向下一个节点,prev 指针域指向前一个节点
  • 头节点的 prev 指向末尾节点,末尾节点的 next 指向头节点

其代码实现如下:

class DoubleCircleLink {
public:
    DoubleCircleLink();
    ~DoubleCircleLink();
  
    // 头插法
    void push_front(int val);
    // 尾插法
    void push_back(int val);
    // 删除
    void remove(int val);
    // 查找
    bool find(int val);
    // 打印
    void print() const;

private:
    // 节点类型
    struct Node {
        Node(int data = 0)
            : data_(data)
            , next_(nullptr)
            , prev_(nullptr) { }
        int   data_;
        Node* next_;
        Node* prev_;
    };

    Node* head_;
};

DoubleCircleLink::DoubleCircleLink()
    : head_(new Node()) {
    head_->next_ = head_;
    head_->prev_ = head_;
}

DoubleCircleLink::~DoubleCircleLink() {
    Node* cur = head_->next_;
    while (cur != head_) {
        head_->next_      = cur->next_;
        cur->next_->prev_ = head_;
        delete cur;
        cur = head_->next_;
    }
    delete head_;
    head_ = nullptr;
}

void DoubleCircleLink::push_back(int val) {
    Node* node          = new Node(val);
    head_->prev_->next_ = node;
    node->next_         = head_;
    node->prev_         = head_->prev_;
    head_->prev_        = node;
}
  
void DoubleCircleLink::push_front(int val) {
    Node* node   = new Node(val);
    Node* nxt    = head_->next_;
    node->next_  = nxt;
    node->prev_  = head_;
    nxt->prev_   = node;
    head_->next_ = node;
}

void DoubleCircleLink::remove(int val) {
    Node* cur = head_->next_;
    while (cur != head_) {
        if (cur->data_ == val) {
            cur->prev_->next_ = cur->next_;
            cur->next_->prev_ = cur->prev_;
            delete cur;
            break;
        }
        cur = cur->next_;
    }
}
  
bool DoubleCircleLink::find(int val) {
    Node* cur = head_->next_;
    while (cur != head_) {
        if (cur->data_ == val) {
            return true;
        }
        cur = cur->next_;
    }
    return false;
}

void DoubleCircleLink::print() const {
    Node* cur = head_->next_;
    while (cur != head_) {
        cout << cur->data_ << "->";
        cur = cur->next_;
    }
    cout << endl;
}

2. 常见的算法问题

2.1 单链表逆序

如果采用暴力的方法,可以定义一个动态数组,然后遍历单链表,将单链表中的每个元素记录下来,然后使用双指针对数组进行逆序操作,完成之后又将数组变回单链表。如果采用这种方法,那么将会消耗掉很多的额外空间,造成资源的浪费。

更好的方法是进行原地逆序,即不采用额外的存储空间。其过程是对于头节点之后的所有元素,采用头插法依次插入到链表中即可,如下图所示:

练习题目: 206. 反转链表 - 力扣(LeetCode)

2.2 合并两个有序链表

由于两个链表是有序的,因此我们可以采用双指针的思想,对于要合并的链表,定义两个指针,一个指向当前节点,另一个指向当前节点的前驱节点,而对于另一个链表,则只需一个指针遍历即可,如下图所示:

其过程如下:

  1. 如果 point3 的数值小于 point1 的数值,执行 2,否则执行 8;
  2. point2 的 next 指向 point3;
  3. head2 的 next 指向 point3 的 next;
  4. point3 的 next 指向 point1;
  5. point2 移动到下一个元素;
  6. point3 指向 head2 的下一个元素;
  7. 重复步骤 1;
  8. point1 和 point2 均移动到下一个元素;

练习题目: 21. 合并两个有序链表 - 力扣(LeetCode)

2.3 访问倒数第k个元素

如果采用最暴力的方法,先遍历一次单链表,得到单链表的长度,然后再从头节点开始遍历,直到找到倒数第 k 个元素,那么其需要遍历两次单链表。

更好的一种方法是,使用双指针的思想,定义两个指针,一个快指针,一个慢指针。先让快指针找到正数第 k 个元素,然后慢指针从头开始走,如下图所示:

然后快、慢指针一块走,当快指针指向 NULL 的时候,慢指针所指的元素即为倒数第 k 个元素,如下图所示:

练习题目:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

2.4 判断链表是否有环

判断一个单链表是否有环,我们依然可以采用双指针的思想,来分别定义一个快指针、一个慢指针,每一次,快指针走 2 步,而慢指针走 1 步。这就如同我们学过的 "追及相遇问题",即两个人在操场上跑步,一个人跑的快,一个人跑的慢,从同一起点开始跑,两个人一定会在某一点处相遇。

如果要找到环的入口节点,那么该如何做?

我们先定义几个变量,从头节点到环入口的距离我们定义为变量 \(x\), 从环入口节点到相遇节点的距离定义为变量 \(y\),从相遇节点到链表末节点的距离定义为变量 \(z\),如图所示:

图中红色节点即为快、慢指针的相遇节点。由于快、慢指针相遇时,快指针所走的距离 \(s_1\) 为慢指针所走距离 \(s_2\) 的 2 倍,即存在 \(s_1=2*s_2\),且由图可知:\(s_1=x+y+z+y\),而 \(s_2=x+y\),那么有如下推导过程:

\[s_1=s_2 \]

\[x+y+z+y=2x+2y \]

\[z=x \]

由于此时快慢指针均指向相遇节点,即图中的红色节点。那么让慢指针指向链表的头节点,慢指针和快指针同时向后走,一次走一步,两个指针相遇时,即为环入口节点。

练习题目: 剑指 Offer II 022. 链表中环的入口节点 - 力扣(LeetCode)

2.5 约瑟夫环

约瑟夫环是一个数学的应用问题: 已知 n 个人(以编号 1,2,3...n 分别表示)围坐在一张圆桌周围,从编号为 k 的人开始报数,数到 m 的那个人出列,它的下一个人又从 1 开始报数,数到 m 的那个人又出列,如此规律重复下去,直到圆桌周围的人全部出列,输出人的出列顺序。

约瑟夫环的典型应用就是单向循环链表

3. STL 实现

3.1 list

STL 的 list 实现了双向循环链表数据结构,其本身和 list 的节点是不同的结构,其节点(node)结构如下:

template<class T>
struct __list_node {
    typedef void* void_pointer;
    void_pointer prev;
    void_pointer next;
    T data;
};

显然,这是一个双向链表,且是环状双向链表,如下图所示:

list 不能像 vector 一样使用普通指针作为迭代器,由于其是双向链表,所以迭代器必须具备前移、后移的能力,所以 list 提供的是 Bidirectional Iterators。而且,插入操作和接合操作都不会造成原有的 list 迭代器失效。甚至 list 的元素删除操作,也只有 “指向被删除元素” 的那个迭代器失效,其他迭代器不受任何影响。

3.2 slist

slist 是 SGI STL 所提供的单向链表,并不在标准规格之内。

其与 list 的主要区别在于:slist的迭代器属于单向的 Forward Iterator,而后者的迭代器属于双向的 Bidirectional Iterator。共同的特点是:插入、移除、接合等操作并不会造成原有的迭代器失效。

如图为 slist 的节点和迭代器的设计架构:

注意:比较两个迭代器是否等同时,由于 __slist_iterator 并未对 operator== 实施重载,所以会调用 __slist_iterator_base::operator==。根据其中定义可以发现两个 slist 迭代器是否相等取决于 __slist_node_base* node 是否相等。

operator*() 的定义则是通过将 node 强转为派生类来实现的:

template<class T, class Ref, class Ptr>
struct __slist_iterator : public __slist_iterator_base {
    typedef __slist_node<T> list_node;
    typedef Ref reference;
    reference operator*() const { return ((list_node*)node)->data; }
};

在 slist 的源码中,end() 的定义有所不同:

template<class T, class Alloc=alloc>
class slist {
    iterator end() { return iterator(0); }
};

如下图所示,其定义为 0:

posted @ 2022-12-13 23:19  Leaos  阅读(145)  评论(0编辑  收藏  举报