十二,数据结构-链表
定义
链表是最简单的基于节点的数据结构。节点表示散步在内存中的互相连接的数据,在链表中,每个节点都表示其中的一个元素。这个元素中除了信息内容之外,还有指向下一个/前一个节点的内存地址信息(指针形式)。
实现
这里先考虑单向链表的实现。首先是链表节点的实现:
class Node {
public:int data;unique_ptr<Node> nextNode;Node(int val) : data(val), nextNode(nullptr) {}
};
其实只使用上述的节点类即可以创建链表,但还需要一个简单的方法告知链表的开始位置,所以再创建一个LinkedList类,链表尾节点的下一节点指针为空,即nullptr。
class LinkedList {
private:unique_ptr<Node> firstNode;public:LinkedList(Node* node) : firstNode{ node } {}
};
链表的读取
数组的读取时间复杂度为O(1),而上述链表由于只记录了链表的第一个位置,因此只能遍历整个链表以读取指定位置的节点,其时间复杂度为O(N)。
int read(int index) const {if (index < 0) {throw out_of_range("Index must be non-negative.");}Node* current = firstNode.get();int count = 0;while (current) {if (count == index) {return current->data;}current = current->nextNode.get();++count;}throw out_of_range("Index exceeds list length.");}
链表的查找
和读取过程类似,链表的查找也是从第一个节点开始遍历所有节点,并检查节点的值是否就是要查找的值。
int indexOf(int value) const {Node* current = firstNode.get();int count = 0;while (current) {if (current->data == value) {return count;}current = current->nextNode.get();++count;}throw out_of_range("Index exceeds list length.");}
链表的插入
特定情况下,链表插入速度优于数组。当需要在数组头插入数据时由于需要右移所有元素,时间复杂度是O(N),但在链表头插入时间复杂度为O(1)。理论上在链表的任何位置插入一个节点,时间复杂度为O(1),但插入前还有个查找的过程,因此当需要在链表尾插入数据时,则其时间复杂度为O(N+1)。
void insert(int index, int val) {if (index < 0) {throw out_of_range("Index must be non-negative.");}auto newNode = make_unique<Node>(val);if (index == 0) {newNode->nextNode = move(firstNode);firstNode = move(newNode);return;}Node* current = firstNode.get();int count = 0;while (current && count < index - 1) {current = current->nextNode.get();count++;}if (!current) {throw out_of_range("Index exceeds list length.");}newNode->nextNode = move(current->nextNode);current->nextNode = move(newNode);}
链表的删除
和插入一样,链表的删除也有其优势,即删除表头节点时,只需要让链表的firstNode指向第二个节点。而删除最后一个节点时,其步骤也只需要一步,但因为需要先遍历到最后一个节点,其时间复杂度仍然为O(N)。
void removeAt(int index) {if (index < 0) {throw out_of_range("Index must be non-negative.");}if (!firstNode) {throw out_of_range("List is empty.");}if (index == 0) {firstNode = move(firstNode->nextNode);return;}Node* current = firstNode.get();int count = 0;while (current->nextNode && count < index - 1) {current = current->nextNode.get();count++;}if (!current->nextNode || count != index - 1) {throw out_of_range("Index exceeds list length.");}current->nextNode = move(current->nextNode->nextNode);}
如果设置为双链表的话,则在链表头或则链表尾进行删除和插入操作,都只需要花O(1)时间,和单链表不同的是,双链表节点需要额外存储前一个节点的内存地址的指针,同时双链表还要知道最后一个节点。双链表实现如下:
class DNode {
public:int data;unique_ptr<DNode> next;DNode* prev;DNode(int val) : data(val), next(nullptr), prev(nullptr) {}
};class DoublyLinkedList {
private:unique_ptr<DNode> head;DNode* tail;public:DoublyLinkedList() : head(nullptr), tail(nullptr) {}void append(int val) {auto newNode = make_unique<DNode>(val);if (!head) {tail = newNode.get();head = move(newNode);return;}newNode->prev = tail;tail->next = move(newNode);tail = tail->next.get();}void insert(int index, int val) {if (index < 0) throw out_of_range("Index must be non-negative.");auto newNode = make_unique<DNode>(val);if (index == 0) {newNode->next = move(head);if (newNode->next) newNode->next->prev = newNode.get();head = move(newNode);if (!tail) tail = head.get();return;}DNode* current = head.get();int count = 0;while (current && count < index - 1) {current = current->next.get();count++;}if (!current) throw out_of_range("Index exceeds list length.");newNode->next = move(current->next);if (newNode->next) newNode->next->prev = newNode.get();newNode->prev = current;current->next = move(newNode);if (!current->next->next) tail = current->next.get();}void removeAt(int index) {if (index < 0 || !head) throw out_of_range("Invalid index or empty list.");if (index == 0) {head = move(head->next);if (head) head->prev = nullptr;else tail = nullptr;return;}DNode* current = head.get();int count = 0;while (current && count < index) {current = current->next.get();count++;}if (!current) throw out_of_range("Index exceeds list length.");DNode* prevNode = current->prev;if (current->next) {current->next->prev = prevNode;prevNode->next = move(current->next);}else {prevNode->next = nullptr;tail = prevNode;}}
};
前文中提到的抽象数据结构队列也可以用双链表实现,不仅满足FIFO,同时其效率为O(1)。这里就不再赘述其实现。