Chap21 Linked Lists
- linked list—a data structure that is a linear sequence of nodes connected by pointers
- 需要LinkedList class, 也需要Node class (private inner class of LinkedList) --> 一个class里面可以有别的class
template<typename T>
class LinkedList {
class Node {
public:
T data;
Node * next;
};
Node * head;
public:
LinkedList() : head(NULL) {}
};
- 编译器要求在类型完全声明(即到达结构或类声明的右花括号)之前,只能使用指向该类型的指针(recursive)
- Node类是public,这样LinkedList可以直接access,Node is private to the LinkedList,所以不用担心外部影响
- 如果list为空,head为NULL
- 可以通过改变指针所指向的位置来添加更多的元素(在任何位置),而不是通过移动元素
- 双向链表使反向遍历链表更加容易
template<typename T>
class LinkedList {
class Node {
public:
T data;
Node * next;
Node * prev;
};
Node * head;
Node * tail;
size_t size;
public:
LinkedList() : head(NULL), tail(NULL), size(0) {}
};
- circular list循环列表,没有最后一个节点,而是节点组成一个圆。圆形列表可以是单链的,也可以是双链的——通过反向链接,可以很容易地从相反的方向遍历圆形
1 Linked List Basic Operations
1.1 Add to Front
- Adding to the front of a singly linked list
- 时间复杂度O(1)
- Adding to the front of a doubly linked list
prev直接是NULL,因为在最前插入没有prev,Node构造器来自动完成
1.2 Add to Back
- Adding to the back of a doubly linked list
1.3 Searching
- 如果想要list的第i个element,通常从头节点开始,遍历每个节点,沿着下一个指针从一个节点到下一个节点,需要O(N), 而数组只需要O(1),因为可以accsess a particular index
- 将轻松访问特定索引的能力转换为无需复制元素就可以修改结构的能力
1.4 Copying
- deep copy(copy constructor或copying assignment operator)需要遍历它的节点,创建具有相同数据的新节点,并将它们链接在一起,以便项以相同的顺序出现
- 一种方法是以相反的顺序遍历我们的源列表,并将每个项添加到目标列表的前面(即调用已经编写的addToFront方法)。如果列表支持简单的反向迭代(即双向链接),那么这个选项就简单而有效
- 也可以遍历源列表,并将每一项添加到目标列表的后面。如果列表支持addToBack in O(1)(即有尾部指针),那么这个选项也很简单和有效。
- 如果列表没有尾指针,可以通过在执行复制时保留一个显式的指向列表末尾的指针来有效地执行这一操作——实际上是复制方法的局部临时尾指针。
- 也可以重新设计列表以包含一个尾部指针
1.5 Destruction
2 Insert in Sorted Order
- 3种方法
2.1 Pointer to Node Before
- 在想要添加的节点之前搜索链表中的节点。我们有一个Node * current并遍历我们的list,寻找the situation where we want to set current’s next to the node we want to add,然后执行指针操作来插入新节点
- corner case --> 想要添加到list的前面(因为想要更改head指针,而不是某个节点的下一个字段),必须单独处理
- Adding in sorted order to a linked list by searching for the node before where we want to add
- 截图内容有错:应该是data > curr->next->data
2.2 Recursion
template<typename T>
class LinkedList {
//other parts elided
private:
class Node {
public:
T data;
Node * next;
Node(const T & d) : data(d), next(NULL) {}
Node(const T & d, Node * n): data(d), next(n) {}
};
//private recursive helper method
Node * addSorted(const T & data, Node * current) {
//base case: insert before current
if (current == NULL || data < current->data) {
return new Node(data, current);
}
//recursive case: add to the rest of list
//then update current’s next
current->next = addSorted(data, current->next);
return current;
}
public:
//public addSorted method: just takes data to add,
//has private recursive helper do the work
void addSorted(const T & data) {
head = addSorted(data, head);
}
};
这里假设有个空链表,head为NULL
2.3 Pointer to a Pointer
3 Removing from a List
3.1 Remove from Front or Back
3.1.1 从Front删除O(1)
- 从Front删除O(1) --> 在检查列表不是空的之后,将head指针的当前值存储在一个临时变量中,然后将head指针更新到head->next,然后删除原始的head节点(我们将其存储在临时变量中正是为了这个目的) --> 参照1.5 destruction
- 如果列表是双向链接的,如果列表不是空的,我们需要更新new head node的previous field。同样,如果我们的列表有一个尾指针,我们需要检查它现在是否为空,如果是,将尾指针更新为NULL
3.1.2 从Back删除
- 如果链表是双向链表并且有一个尾部指针,那么从链表的后面删除O(1)。在这种情况下,与remove from front类似,除了是在尾部而不是头部操作,使用previous field而不是next field。然后,更新new tail的next field(如果非空),如果列表为空,则将head设置为NULL
- 如果列表是单链接的(或没有尾指针),我们需要遵循一种O(N)算法来删除一个元素(后面看到)。如果需要频繁地从列表后面删除,最好使列表具有双链接并有一个尾部指针(可以实现O(1))
3.2 Other Removals: Pointer to Node Before
- 如果想要删除元素的数据是42,则要找到一个node,current->next->data是42。如果列表是双向链接的,则找到想要的节点,然后后退一个节点(通过previous pointer)
- 然而,删除列表中的第一个节点仍然是一种特殊情况——需要更新head pointer,而不是任何节点的next field
- 找到前一个节点后,首先需要用temp variable指向要删除节点(即current->next),以便稍后删除它。然后更新current->next to be current->next->next,这将从列表中删除所需删除的节点
- 如果是双向链表,则需要更新current->next->prev(如果current->next不是NULL)。如果有一个尾指针,需要在删除列表的最后一个节点时更新current->next->prev
3.3 Other Removals: Recursion
template<typename T>
class LinkedList {
//other parts elided
private:
class Node {
public:
T data;
Node * next;
Node(const T & d) : data(d), next(NULL) {}
Node(const T & d, Node * n) : data(d), next(n) {}
};
//private recursive helper
Node * remove(const T & data, Node * current) {
if (current == NULL) { //base case: empty list
return NULL; //answer is empty list
}
if (data == current->data) { //base case: node to remove
Node * ans = current->next; //answer will be "everything after"
delete current; //delete node we are removing
return ans; //return our answer
}
//revursive case: remove from rest of list, then update current's next
current->next = remove(data, current->next);
return current;
}
public:
//public remove method: just take data to remove, has private recursive helper do the work
void remove(const T & data) {
head = remove(data, head);
}
};
3.4 Other Removals: Pointer to a Pointer
- 使用Node **,它从&head开始,表示“我们可能想要更改的框”,然后沿着列表迭代,找到要删除的位置。一旦找到它,就执行适当的指针操作(将所指向的方框更改为要删除的node之后的下一个node)
3.5 Remove All Occurrences
template<typename T>
class LinkedList {
//other parts elided
private:
//private recursive helper
//templated over whether to remove all or one
template<bool removeAll>
Node * remove(const T & data, Node * current) {
if (current == NULL) {
return NULL;
}
if (data == current->data) {
Node * ans;
if (removeAll) {
ans = remove<removeAll>(data, current->next);
}
else {
ans = current->next;
}
delete current;
return ans;
}
current->next = remove<removeAll>(data, current->next);
return current;
}
public:
//public remove methods: just takes data to remove,
//has private recursive helper do the work
void remove(const T & data) {
head = remove<false>(data, head);
}
void removeAll(const T & data) {
head = remove<true>(data, head);
}
};
4 Iterators
- iterators --> objects that 封装数据结构中的位置,并提供了一种无需了解结构内部就可以访问和移动元素的方式
- 链表的lterator可以使得下面为O(N) algorithm
LinkedList<T>::iterator it = myList.begin();
while (it != myList.end()) {
T & current = *it;
//do stuff with current
++it;
}
4.1 Implementation
- 在LinkedList类中编写一个内部类来实现迭代器。作为LinkedList的内部类,迭代器是类的一部分,因此它可以了解列表的实现细节(并访问私有成员)
template<typename T>
class LinkedList {
class Node {
//contents of Node elided
};
public:
class iterator {
Node * current;
public:
iterator() : current(NULL) {}
explicit iterator(Node * c) : current(c) {} //explicit关键字修饰的类构造函数,不能进行自动地隐式类型转换,只能显式地进行类型转换
iterator & operator++() { //++i
current = current->next;
return *this;
}
iterator & operator++(int) { //i++
iterator ans(current);
current = current->next;
return ans;
}
T & operator*() const {
return current->data;
}
T * operator->() const {
return ¤t->data;
}
bool operator!=(const iterator & rhs) const {
return current != rhs.current;
}
bool operator==(const iterator & rhs) const {
return current == rhs.current;
}
//possibly other methods
};
iterator begin() {
return iterator(head);
}
iterator end() {
return iterator(NULL);
}
//other things in LinkedList elided
};
/*
explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了
*/
- explicit关键字详解
- 以上迭代器是forward iterator,只能在前向方向上遍历结构
- 如果是双向链表,则添加–操作符使迭代器成为双向迭代器,该操作符将沿着previous links backwards遍历列表
- const_iterator --> read-only access to the list --> overload begin and end
template<typename T>
class LinkedList {
class Node {
//contents of Node elided
};
public:
class iterator {
//as above
};
class const_iterator {
//similar, but const Node *
//returns const &s/const *s
};
iterator begin() { return iterator(head); }
iterator end() { return iterator(NULL); }
const_iterator begin() const { return const_iterator(head); }
const_iterator end() const { return const_iterator(NULL); }
//other things in LinkedList elided
};
5 Uses for ADTs
5.1 Stacks
- stack是LIFO后进先出,可以通过在链表的同一末端添加和删除来实现这种行为(添加到前面和从前面删除通常是最简单的 --> O(1))
5.2 Queues
- FIFO先进先出,可以通过在链表的两端进行添加和删除来实现这种行为——如果向链表的后面(即尾部)添加,那么就从前面(即头部)删除。如果有尾指针,就可以实现这两个操作O(1)
5.3 Sets
- 通过将元素添加到列表中来添加元素;通过遍历列表中的元素来检查set是否包含元素,测试每个元素是否匹配(如果匹配返回true,如果不匹配返回false);和通过从列表中删除元素来删除元素
- 如果添加一个元素两次,然后又删除了它(在一个集合中,正确的行为是元素不再在集合中)。可以用两种方法来解决这个问题:要么在添加时检查重复,要么在删除时删除所有的重复。前者的优点是,列表会更小,但缺点是,添加变成了一个O(N)操作(必须遍历列表)
5.4 Maps
- 通过在每个节点中存储键值对来实现一个带有链表的映射。
- 通过将键/值对插入到列表中来添加键/值对(可能首先删除旧的映射);通过遍历列表来查找键的值(检查每个节点是否匹配,并返回匹配的值);通过找到匹配的节点并删除它,从列表中删除一个键
6 STL List
- STL有内置的std::list class,是doubly linked list
- http://www.cplusplus.com/reference/list/list/