数据结构之 List
写在前面
和vector/array一样,链表/列表(list)也是线性表中的一种,即每个元素的直接前驱和直接后继都唯一的线性逻辑结构。
链表/列表List
- 这里依然是放在一起讲,链表是通常的数据结构课上常见的一种结构,而列表是STL中已经封装好的一个模板类。所以我们在学习链表的时候,完全可以看看STL中的源码,对比学习,借鉴一些比较好的思想。值得注意的是,本文中的列表只按照STL的思想来,但代码是自己实现的,和源代码并不一致。
- 与vector一样,列表也是由集合S中的元素依次排列的一个序列,这些元素分散地存储在存储空间中,因此难以直接根据地址直接访问到每一个元素。为此,每个元素存储时需携带一些额外的信息:它的前驱、后继等。
- 同样地,参考vector,可以定义列表的前驱、后继、直接前驱、直接后继、前缀、后缀等。
- 根据所包含额外信息的不同,链表可分为
- 单链表:只包含指向后继结点的指针
- 双链表:包含指向前驱结点和后继节点的指针
- 单循环链表:最后一个结点的后继指针指向头结点
- 双循环链表:最后一个结点的后继指针指向头结点,第一个结点的前驱指针指向最后一个结点
- 在已经封装好的列表中,是按照双链表来设置结点对象的,用的时候按需取用就好。
list的实现
结点的图形化表示
结点类
#ifndef LISTNODE_H
#define LISTNODE_H
typedef int Rank;
#define ListNodePosi(T) ListNode<T>*
template <typename T>
struct ListNode {
// member variable
T data;
ListNodePosi(T) pred; //前驱
ListNodePosi(T) succ; //后继
// member function
ListNode() {}
ListNode(T e, ListNodePosi(T) p = NULL, ListNodePosi(T) s = NULL)
: data(e), pred(p), succ(s) {}
ListNodePosi(T) insertAsPred(T const& e);
ListNodePosi(T) insertAsSuss(T const& e);
};
template <typename T>
ListNodePosi(T) ListNode<T>::insertAsPred(T const& e) {
ListNodePosi(T) node = new ListNode<T>(e);
node->succ = pred->succ;
node->pred = pred;
pred->succ = node;
pred = node;
}
template <typename T>
ListNodePosi(T) ListNode<T>::insertAsSuss(T const& e) {
ListNodePosi(T) node = new ListNode<T>(e);
node->pred = succ->pred;
node->succ = succ;
succ->pred = node;
succ = node;
}
#endif
列表的图形化表示
- 这里需要说明的是,链表分为带表头和不带表头两种,如上图所示。在具体操作时两种形式有所区别,一般来说,带表头的操作方便一些,列表中就是带表头的。
列表类
#ifndef LIST_H
#define LIST_H
// #include "ListNode.cpp"
#include "ListNode.h"
template <typename T>
class List {
private:
int _size;
ListNodePosi(T) header;
ListNodePosi(T) tailer;
protected:
void init(); // initial after creating
int clear(); // clear all nodes
void copyNodes(ListNodePosi(T), int); // copy n nodes from p
void merge(ListNodePosi(T) &,
int,
List<T>&,
ListNodePosi(T),
int); // merge two sorted lists
//有序列表的归并:当前列表中自p起的n个元素,与列表L中自q起的m个元素归并
void mergeSort(ListNodePosi(T) &, int); //对从p开始的n个节点归并排序
void selectionSort(ListNodePosi(T), int); //对从p开始的n个节点选择排序
void insertionSort(ListNodePosi(T), int); //对从p开始的n个节点插入排序
public:
List() { init(); }
List(List<T> const& L); // copy whole List
List(List<T> const& L, Rank r, Rank n); // copy n elements in L from r
List(ListNodePosi(T) p, int n);
~List();
Rank size() const { return _size; }
bool empty() const { return _size <= 0; }
T& operator[](int r) const;
ListNodePosi(T) fisrt() const { return header->succ; }
ListNodePosi(T) last() const { return tailer->pred; }
bool valid(ListNodePosi(T) p) {
return p && (header != p) && (tailer != p);
}
int disordered() const;
ListNodePosi(T) find(T const& e) const { find(e, _size, tailer); }
ListNodePosi(T)
find(T const& e,
int n,
ListNodePosi(T) p); // find e in range n before p from p
ListNodePosi(T) search(T const& e) const { search(e, _size, tailer); }
ListNodePosi(T) search(T const& e, int n, ListNodePosi(T) p);
ListNodePosi(T)
selextMax(ListNodePosi(T) p, int n); // select max node from p to p-n
ListNodePosi(T) selextMax() { return selextMax(header->succ, _size); }
ListNodePosi(T) insertAsFirst(T const& e);
ListNodePosi(T) insertAsFirst(T const& e);
ListNodePosi(T) insertBefore(ListNodePosi(T) p, T const& e);
ListNodePosi(T) insertAfter(ListNodePosi(T) p, T const& e);
T remove(ListNodePosi(T) p);
void merge(List<T>& L) {
merge(fisrt(), _size, L, L.fisrt(), L.size());
} // merge the whole list L
void sort(ListNodePosi(T) p, int n); // sort the part of list
void sort() { sort(first(), _size); }
int deduplicate(); // deduplicate a unordered list
int uniquify(); // uniquify an ordered list
void reverse(); // reverse the list
void traverse(void (*)(T&));
template <typename VST> // operator
void traverse(VST&);
};
#endif
与Vector的比较
- 循秩访问与循位置访问
- vector最大的特点就是循秩访问,也就是可以根据元素的物理地址直接访问该元素。
- 由于不能直接给出元素所在的物理地址,因此当需要访问某个元素时,只能从头结点依次向后查询,直到找到该元素或者访问到最后一个结点为止,后一种情况时该元素不存在与列表中。
- 带来的利弊
- vector可以随机访问,因此当有很多的读取等静态操作时,使用vector效率较高
- 但对于insert和remove等动态操作,vector的实现时间复杂度为O(n),而list只需新开辟一块地址空间存入新的元素并修改相邻元素的指针或者修改完指针再回收地址空间即可实现,其时间复杂度不过O(1)
- 综上所述,当需求中静态访问操作较多时,使用vector效率高;当需求中增删操作较多时,使用list效率更高。