线性表 -2 链表

崇高与荒谬,
常常如此紧密相关,
以至于要区分它们都非常困难。
— Tom Paine

线性表之二

链表

既然我们想让相邻元素之间有足够的位置, 那么干脆所有的元素都不要考虑相邻位置了, 哪里有空位, 就到哪里, 只是让每个元素知道知道它下一个元素的位置, 这样从第一个元素开始, 所有的元素都可以通过遍历找到。


链表的出现是为了解决顺序表的一些不足, 当顺序表在进行插入和删除操作时, 需要移动大量的元素, 这导致了这两个操作的效率极其低下, 而之所以需要移动大量元素, 原因就在于:
相邻的两个元素之间没有空隙, 在内存中的位置也是紧紧挨着的, 因此无法快速介入。
既然我们想让相邻元素之间有足够的位置, 那么干脆所有的元素都不要考虑相邻位置了, 哪里有空位, 就到哪里, 只是让每个元素知道知道它下一个元素的位置, 这样从第一个元素开始, 所有的元素都可以通过遍历找到。

链表一般分为单链表双链表, 这两种链表的区别, 只有链表中存储的元素不太相同:
链表中的元素称为结点,
在单链表中, 一个结点存储了它的元素数据, 也存储了它下一个元素的地址, 存储的信息叫数据域, 存储的下一个元素的地址叫指针域;
在双链表中, 结点有数据域, 还拥有两个指针域, 分别存储了它上一个和下一个元素的地址

链表的头尾插入期望时间复杂度是 o ( 1 ) o(1) o(1), 删除,插入和按索引寻找元素最坏是 o ( n ) o(n) o(n), 最好则是 o ( 1 ) o(1) o(1)

单链表

首先呈上老师的代码

#include <stdio.h>
#include <malloc.h>

/**
 * Linked list of characters. The key is data.
 */
typedef struct LinkNode{
	char data;
	struct LinkNode *next;
} LNode, *LinkList, *NodePtr;

/**
 * Initialize the list with a header.
 * @return The pointer to the header.
 */
LinkList initLinkList(){
	NodePtr tempHeader = (NodePtr)malloc(sizeof(LNode));
	tempHeader->data = '\0';
	tempHeader->next = NULL;
	return tempHeader;
}// Of initLinkList

/**
 * Print the list.
 * @param paraHeader The header of the list.
 */
void printList(NodePtr paraHeader){
	NodePtr p = paraHeader->next;
	while (p != NULL) {
		printf("%c", p->data);
		p = p->next;
	}// Of while
	printf("\r\n");
}// Of printList

/**
 * Add an element to the tail.
 * @param paraHeader The header of the list.
 * @param paraChar The given char.
 */
void appendElement(NodePtr paraHeader, char paraChar){
	NodePtr p, q;

	// Step 1. Construct a new node.
	q = (NodePtr)malloc(sizeof(LNode));
	q->data = paraChar;
	q->next = NULL;

	// Step 2. Search to the tail.
	p = paraHeader;
	while (p->next != NULL) {
		p = p->next;
	}// Of while

	// Step 3. Now add/link.
	p->next = q;
}// Of appendElement

/**
 * Insert an element to the given position.
 * @param paraHeader The header of the list.
 * @param paraChar The given char.
 * @param paraPosition The given position.
 */
void insertElement(NodePtr paraHeader, char paraChar, int paraPosition){
	NodePtr p, q;

	// Step 1. Search to the position.
	p = paraHeader;
	for (int i = 0; i < paraPosition; i ++) {
		p = p->next;
		if (p == NULL) {
			printf("The position %d is beyond the scope of the list.", paraPosition);
			return;
		}// Of if
	} // Of for i

	// Step 2. Construct a new node.
	q = (NodePtr)malloc(sizeof(LNode));
	q->data = paraChar;

	// Step 3. Now link.
	printf("linking\r\n");
	q->next = p->next;
	p->next = q;
}// Of insertElement

/**
 * Delete an element from the list.
 * @param paraHeader The header of the list.
 * @param paraChar The given char.
 */
void deleteElement(NodePtr paraHeader, char paraChar){
	NodePtr p, q;
	p = paraHeader;
	while ((p->next != NULL) && (p->next->data != paraChar)){
		p = p->next;
	}// Of while

	if (p->next == NULL) {
		printf("Cannot delete %c\r\n", paraChar);
		return;
	}// Of if

	q = p->next;
	p->next = p->next->next;
	free(q);
}// Of deleteElement

/**
 * Unit test.
 */
void appendInsertDeleteTest(){
	// Step 1. Initialize an empty list.
	LinkList tempList = initLinkList();
	printList(tempList);

	// Step 2. Add some characters.
	appendElement(tempList, 'H');
	appendElement(tempList, 'e');
	appendElement(tempList, 'l');
	appendElement(tempList, 'l');
	appendElement(tempList, 'o');
	appendElement(tempList, '!');
	printList(tempList);

	// Step 3. Delete some characters (the first occurrence).
	deleteElement(tempList, 'e');
	deleteElement(tempList, 'a');
	deleteElement(tempList, 'o');
	printList(tempList);

	// Step 4. Insert to a given position.
	insertElement(tempList, 'o', 1);
	printList(tempList);
}// Of appendInsertDeleteTest

/**
 * Address test: beyond the book.
 */
void basicAddressTest(){
	LNode tempNode1, tempNode2;

	tempNode1.data = 4;
	tempNode1.next = NULL;

	tempNode2.data = 6;
	tempNode2.next = NULL;

	printf("The first node: %d, %d, %d\r\n",
		&tempNode1, &tempNode1.data, &tempNode1.next);
	printf("The second node: %d, %d, %d\r\n",
		&tempNode2, &tempNode2.data, &tempNode2.next);

	tempNode1.next = &tempNode2;
}// Of basicAddressTest

/**
 * The entrance.
 */
int main(){
	appendInsertDeleteTest();
}// Of main

结点的构建

单链表一般使用动态分配内存, 所以无需给单链表指定最大容量, 在C语言中, 可以使用malloc来为结点分配空间, 在C++中, 也可以使用new来为结点分配空间。

使用malloc的语法如下

typedef struct LinkNode{
	char data;
	struct LinkNode *next;
} LNode, *LinkList, *NodePtr;

NodePtr q = (NodePtr)malloc(sizeof(LNode));

在C++中, new一般配合类来使用:

class Node {
	private:
		ElemType data;
		Node* next;
		
	public:
		virtual ~Node() {
				
		}
		
		Node();
		
		Node(ElemType data, Node* next) {
			this -> data = data;
			this -> next = next;	
		}
		
		ElemType getData() {
			return this -> data;
		}
		
		void setData(ElemType elem) {
			this -> data = elem;
		}
		
		Node* getNext() {
			return this -> next;
		}
		
		void setNext(Node* node) {
			this -> next = node;
		}
};

Node* node = new Node(elem, nullptr);

值得注意的是, 释放malloc分配的内存需要使用free, 而new分配的需要使用delete, 在C++程序结束后,两者不能混用, 否则会导致意料之外的结果。

头文件和定义
#include <iostream>
#include <vector>
#define remilia int
#define isMyWife main

using namespace std;

template<class ElemType>
class LinkedList {
	private:
		class Node {
			private:
				ElemType data;
				Node* next;
				
			public:
				virtual ~Node() {
						
				}
				
				Node();
				
				Node(ElemType data, Node* next) {
					this -> data = data;
					this -> next = next;	
				}
				
				ElemType getData() {
					return this -> data;
				}
				
				void setData(ElemType elem) {
					this -> data = elem;
				}
				
				Node* getNext() {
					return this -> next;
				}
				
				void setNext(Node* node) {
					this -> next = node;
				}
		};
		
		int length;
		Node* head;
		Node* tail;
	
	public:
		virtual ~LinkedList();
		LinkedList();
		LinkedList(vector<ElemType> v);
		
		// get the length of linkedlist
		int getLength();
		
		// print all of the linkedlist
		void outputList();
		
		/**
		 * @brief  append a node to the tail
		 * @return the result of appending  1 -> success
		 * @param elem 
		 * the appending is always successful
		 **/
		bool pushBack(ElemType elem);
		
		/**
		 * @brief append a node to the head
		 * @param elem 
		 * @return the result of appending  1 -> success
		 * the appending is always successful
		 **/
		bool pushHead(ElemType elem);
		
		/**
		 * @brief delete a node from head
		 * @return the result of deleting 1 -> success, 0 -> failute
		 * the reason of failure is that the head node isn't existed
		 **/
		bool popHead();
		
		/**
		 * @brief delete a node from tail
		 * @return the result of deleting 1 -> success, 0 -> failute
		 * the reason of failure is that the tail node isn't existed
		 * @return 
		 **/
		bool popBack();
		
		
		// insert an element to the given position
		bool insertElement(int paraPosition, ElemType elem);
		
		/**
		 * @brief delete a node by the paraPosition
		 * @param the position of the node needed deleted
		 * @return 1 -> success, 0 -> fail
		 * the reason of failure is the position out of range
		 **/
		bool deleteElementByParaPosition(int paraPosition); 
		
		/**
		 * @brief clear the list
		 * 
		 **/
		void clear();
}; 
单链表的头尾插入

使用headtail指针记录头尾结点, 那么在头部和尾部的插入时间复杂度都是 o ( 1 ) o(1) o(1)
在这两个指针都为nullptr时, 说明此时链表是空的, 那么我们需要让headtail都指向新添加的结点。
判断头和判断尾的效果是相同的, 它们此时同时为nullptr

if (this -> tail == nullptr) {
		this -> head = this -> tail = new Node(elem, nullptr);
}

其他情况, 当在头部插入时, 我们需要让新添加的结点的后继指向现有的头结点, 然后让新添加的结点成为现有头结点。

template<class ElemType>
bool LinkedList<ElemType>::pushHead(ElemType elem) {
	if (this -> head == nullptr) {
		this -> head = this -> tail = new Node(elem, nullptr);
	} else {
		Node* newTail = new Node(elem, head);
		
		this -> head = newTail;
	}
	
	this -> length++;
	return 1;
}

尾插也是同理

template<class ElemType>
bool LinkedList<ElemType>::pushBack(ElemType elem) {
	// if the tail equals null, the head equals null too
	if (this -> tail == nullptr) {
		this -> head = this -> tail = new Node(elem, nullptr);
	} else {
		Node* newTail = new Node(elem, nullptr);
		
		this -> tail -> setNext(newTail);
		this -> tail = newTail; 
	}
	
	this -> length++;
	return 1;
}
单链表的头尾删除

当链表只有一个元素时, 特判一下, 让头尾指针指向nullptr即可。
其他情况, 当在头部删除时, 让头指针指向现有头结点的后继

template<class ElemType>
bool LinkedList<ElemType>::popHead() {
	if (this -> length == 1) {
		this -> head = this -> tail = nullptr;
	}
	
	if (this -> head == nullptr) {
		return 0;
	} else {
		Node* temp = this -> head;
		this -> head = this -> head -> getNext();
		delete temp;
		return 1;
	}
}

尾部删除同理

template<class ElemType>
bool LinkedList<ElemType>::popBack() {
	if (this -> length == 1) {
		this -> head = this -> tail = nullptr;
	}
	
	if (this -> tail == nullptr) {
		return 0;
	} else {
		Node* temp = this -> head;
		
		while (temp -> getNext() != this -> tail) {
			temp = temp -> getNext();
		}
		
		delete temp -> getNext();
		
		this -> tail = temp;
		return 1;
	}
}
单链表的任意插入

当插入位置是头部时, 调用pushHead()方法即可, 当插入位置是长度 + 1时, 调用pushBack()方法即可。
其他情况, 当我们需要在某个位置插入时, 我们只需要找到它的前驱, 然后让它前驱的后继是它, 让它的后继是它前驱的原本后继, 这样就完成了插入操作。
在这里插入图片描述

template<class ElemType>
bool LinkedList<ElemType>::insertElement(int paraPosition, ElemType elem) {
	if (paraPosition > this -> getLength() + 1) {
		return 0;	
	}
	
	if (paraPosition == 1) {
		return this -> pushHead(elem);
	}
	
	Node* temp = this -> head;
	int recordIndex = 1;
	
	while (recordIndex < paraPosition - 1) {
		temp = temp -> getNext();
		recordIndex++;	
	}
	
	Node* newNode = new Node(elem, temp -> getNext());
	temp -> setNext(newNode);
	this -> length++;
	return 1;
}
单链表的删除

对于第一个结点来说, 只需要将头指针后移一位, 对于其他结点来说, 我们先找到要删除结点的前驱, 然后将此前驱的后继指向要删除结点的后继, 再释放要删除结点的内存即可。
在这里插入图片描述

template<class ElemType>
bool LinkedList<ElemType>::deleteElementByParaPosition(int paraPosition) {
	if (paraPosition > this -> getLength()) {
		return 0;
	}
	
	if (paraPosition == 1) {
		return popHead();
	}
	
	int recordIndex = 1;
	Node* temp = this -> head;
	
	while (recordIndex < paraPosition - 1) {
		temp = temp -> getNext();	
		recordIndex++;
	}
	
	Node* recorder = temp -> getNext();
	temp -> setNext(recorder -> getNext());
	delete recorder;
	return 1;
}
按索引查找值

从头结点开始, 顺序查找即可

template<class ElemType>
ElemType LinkedList<ElemType>::getElement(int paraPosition) {
	if (paraPosition > this -> length) {
		throw "out of range...";
	}
	
	int recordIndex = 1;
	Node* temp = this -> head;
	
	while (recordIndex < paraPosition) {
		temp = temp -> getNext();
		recordIndex++;
	}
	
	return temp -> getData();
}
整表删除

遍历链表, 依次释放每个结点的内存即可, 再将头尾结点设置为nullptr

template<class ElemType>
void LinkedList<ElemType>::clear() {
	Node* temp = this -> head;
	Node* storge = temp;
	
	while (temp != nullptr) {
		temp = temp -> getNext();
		delete storge;
		storge = temp;
	}	
	
	delete storge;
	this -> head = this -> tail = nullptr;
	this -> length = 0;
}
整表输出

遍历链表即可

template<class ElemType>
void LinkedList<ElemType>::outputList() {
	Node* temp = this -> head;
	
	while (temp != nullptr) {
		cout << temp -> getData() << " ";
		temp = temp -> getNext();
	}	
	
	putchar('\n');
}
获取链表长度
template<class ElemType>
int LinkedList<ElemType>::getLength() {
	return this -> length;	
}

这些方法就是单链表的常用操作, 链表的核心思想, 就是每个结点记录下一个结点的地址, 理解了这一点, 那理解链表就不成问题了。

总体代码
#include <iostream>
#include <vector>
#define remilia int
#define isMyWife main

using namespace std;

template<class ElemType>
class LinkedList {
	private:
		class Node {
			private:
				ElemType data;
				Node* next;
				
			public:
				virtual ~Node() {
						
				}
				
				Node();
				
				Node(ElemType data, Node* next) {
					this -> data = data;
					this -> next = next;	
				}
				
				ElemType getData() {
					return this -> data;
				}
				
				void setData(ElemType elem) {
					this -> data = elem;
				}
				
				Node* getNext() {
					return this -> next;
				}
				
				void setNext(Node* node) {
					this -> next = node;
				}
		};
		
		int length;
		Node* head;
		Node* tail;
	
	public:
		virtual ~LinkedList();
		LinkedList();
		LinkedList(vector<ElemType> v);
		
		// get the length of linkedlist
		int getLength();
		
		// print all of the linkedlist
		void outputList();
		
		/**
		 * @brief  append a node to the tail
		 * @return the result of appending  1 -> success
		 * @param elem 
		 * the appending is always successful
		 **/
		bool pushBack(ElemType elem);
		
		/**
		 * @brief append a node to the head
		 * @param elem 
		 * @return the result of appending  1 -> success
		 * the appending is always successful
		 **/
		bool pushHead(ElemType elem);
		
		/**
		 * @brief delete a node from head
		 * @return the result of deleting 1 -> success, 0 -> failute
		 * the reason of failure is that the head node isn't existed
		 **/
		bool popHead();
		
		/**
		 * @brief delete a node from tail
		 * @return the result of deleting 1 -> success, 0 -> failute
		 * the reason of failure is that the tail node isn't existed
		 * @return 
		 **/
		bool popBack();
		
		
		// insert an element to the given position
		bool insertElement(int paraPosition, ElemType elem);
		
		/**
		 * @brief delete a node by the paraPosition
		 * @param the position of the node needed deleted
		 * @return 1 -> success, 0 -> fail
		 * the reason of failure is the position out of range
		 **/
		bool deleteElementByParaPosition(int paraPosition); 
		
		/**
		 * @brief clear the list
		 * 
		 **/
		void clear();
		
		/**
		 * @brief get a element by paraPosition
		 * 
		 * 
		 * @return the element
		 **/
		ElemType getElement(int paraPosition);
}; 


template<class ElemType>
LinkedList<ElemType>::~LinkedList() {
	
}

template<class ElemType>
LinkedList<ElemType>::LinkedList() {
	this -> length = 0;
	this -> head = this -> tail = nullptr;
}

template<class ElemType>
LinkedList<ElemType>::LinkedList(vector<ElemType> v) {
	for (auto e : v) {
		this -> pushBack(e);
	}
}

template<class ElemType>
bool LinkedList<ElemType>::pushBack(ElemType elem) {
	// if the tail equals null, the head equals null too
	if (this -> tail == nullptr) {
		this -> head = this -> tail = new Node(elem, nullptr);
	} else {
		Node* newTail = new Node(elem, nullptr);
		
		this -> tail -> setNext(newTail);
		this -> tail = newTail; 
	}
	
	this -> length++;
	return 1;
}

template<class ElemType>
bool LinkedList<ElemType>::pushHead(ElemType elem) {
	if (this -> head == nullptr) {
		this -> head = this -> tail = new Node(elem, nullptr);
	} else {
		Node* newTail = new Node(elem, head);
		
		this -> head = newTail;
	}
	
	this -> length++;
	return 1;
}

template<class ElemType>
bool LinkedList<ElemType>::popHead() {
	if (this -> length == 1) {
		this -> head = this -> tail = nullptr;
	}
	
	if (this -> head == nullptr) {
		return 0;
	} else {
		Node* temp = this -> head;
		this -> head = this -> head -> getNext();
		delete temp;
		return 1;
	}
}

template<class ElemType>
bool LinkedList<ElemType>::popBack() {
	if (this -> length == 1) {
		this -> head = this -> tail = nullptr;
	}
	
	if (this -> tail == nullptr) {
		return 0;
	} else {
		Node* temp = this -> head;
		
		while (temp -> getNext() != this -> tail) {
			temp = temp -> getNext();
		}
		
		delete temp -> getNext();
		
		this -> tail = temp;
		return 1;
	}
}

template<class ElemType>
void LinkedList<ElemType>::outputList() {
	Node* temp = this -> head;
	
	while (temp != nullptr) {
		cout << temp -> getData() << " ";
		temp = temp -> getNext();
	}	
	
	putchar('\n');
}

template<class ElemType>
int LinkedList<ElemType>::getLength() {
	return this -> length;	
}

template<class ElemType>
void LinkedList<ElemType>::clear() {
	Node* temp = this -> head;
	Node* storge = temp;
	
	while (temp != nullptr) {
		temp = temp -> getNext();
		delete storge;
		storge = temp;
	}	
	
	delete storge;
	this -> head = this -> tail = nullptr;
	this -> length = 0;
}

template<class ElemType>
bool LinkedList<ElemType>::deleteElementByParaPosition(int paraPosition) {
	if (paraPosition > this -> getLength()) {
		return 0;
	}
	
	if (paraPosition == 1) {
		return popHead();
	}
	
	int recordIndex = 1;
	Node* temp = this -> head;
	
	while (recordIndex < paraPosition - 1) {
		temp = temp -> getNext();	
		recordIndex++;
	}
	
	Node* recorder = temp -> getNext();
	temp -> setNext(recorder -> getNext());
	delete recorder;
	return 1;
}

template<class ElemType>
bool LinkedList<ElemType>::insertElement(int paraPosition, ElemType elem) {
	if (paraPosition > this -> getLength() + 1) {
		return 0;	
	}
	
	if (paraPosition == 1) {
		return this -> pushHead(elem);
	}
	
	Node* temp = this -> head;
	int recordIndex = 1;
	
	while (recordIndex < paraPosition - 1) {
		temp = temp -> getNext();
		recordIndex++;	
	}
	
	Node* newNode = new Node(elem, temp -> getNext());
	temp -> setNext(newNode);
	this -> length++;
	return 1;
}

template<class ElemType>
ElemType LinkedList<ElemType>::getElement(int paraPosition) {
	if (paraPosition > this -> length) {
		throw "out of range...";
	}
	
	int recordIndex = 1;
	Node* temp = this -> head;
	
	while (recordIndex < paraPosition) {
		temp = temp -> getNext();
		recordIndex++;
	}
	
	return temp -> getData();
}
	
remilia isMyWife() {
	LinkedList<int> l;
	
	for (int i = 1; i <= 20; i++) {
		for (int j = 1; j <= 10; j++) {
			l.pushBack(j);		
		}
		
		l.insertElement(i, 500);
		l.outputList();
		cout << l.getElement(2) << "\n";
		l.clear();
	}
	
	return 0;
}

双链表

双链表和单链表的区别只有Node有所不同, 在Node中包含两个指针域: 分别指向它的前驱和后继.
老师代码:

#include <stdio.h>
#include <malloc.h>

/**
 * Double linked list of integers. The key is char.
 */
typedef struct DoubleLinkedNode{
	char data;
	struct DoubleLinkedNode *previous;
	struct DoubleLinkedNode *next;
} DLNode, *DLNodePtr;

/**
 * Initialize the list with a header.
 * @return The pointer to the header.
 */
DLNodePtr initLinkList(){
	DLNodePtr tempHeader = (DLNodePtr)malloc(sizeof(struct DoubleLinkedNode));
	tempHeader->data = '\0';
	tempHeader->previous = NULL;
	tempHeader->next = NULL;
	return tempHeader;
}// Of initLinkList

/**
 * Print the list.
 * @param paraHeader The header of the list.
 */
void printList(DLNodePtr paraHeader){
	DLNodePtr p = paraHeader->next;
	while (p != NULL) {
		printf("%c", p->data);
		p = p->next;
	}// Of while
	printf("\r\n");
}// Of printList

/**
 * Insert an element to the given position.
 * @param paraHeader The header of the list.
 * @param paraChar The given char.
 * @param paraPosition The given position.
 */
void insertElement(DLNodePtr paraHeader, char paraChar, int paraPosition){
	DLNodePtr p, q, r;

	// Step 1. Search to the position.
	p = paraHeader;
	for (int i = 0; i < paraPosition; i ++) {
		p = p->next;
		if (p == NULL) {
			printf("The position %d is beyond the scope of the list.", paraPosition);
			return;
		}// Of if
	} // Of for i

	// Step 2. Construct a new node.
	q = (DLNodePtr)malloc(sizeof(struct DoubleLinkedNode));
	q->data = paraChar;

	// Step 3. Now link.
	r = p->next;
	q->next = p->next;
	q->previous = p;
	p->next = q;
	if (r != NULL) {
		r->previous = q;
	}// Of if
}// Of insertElement

/**
 * Delete an element from the list.
 * @param paraHeader The header of the list.
 * @param paraChar The given char.
 */
void deleteElement(DLNodePtr paraHeader, char paraChar){
	DLNodePtr p, q, r;
	p = paraHeader;

	// Step 1. Locate.
	while ((p->next != NULL) && (p->next->data != paraChar)){
		p = p->next;
	}// Of while

	// Step 2. Error check.
	if (p->next == NULL) {
		printf("The char '%c' does not exist.\r\n", paraChar);
		return;
	}// Of if

	// Step 3. Change links.
	q = p->next;
	r = q->next;
	p->next = r;
	if (r != NULL) {
		r->previous = p;
	}// Of if

	// Step 4. Free the space.
	free(q);
}// Of deleteElement

/**
 * Unit test.
 */
void insertDeleteTest(){
	// Step 1. Initialize an empty list.
	DLNodePtr tempList = initLinkList();
	printList(tempList);

	// Step 2. Add some characters.
	insertElement(tempList, 'H', 0);
	insertElement(tempList, 'e', 1);
	insertElement(tempList, 'l', 2);
	insertElement(tempList, 'l', 3);
	insertElement(tempList, 'o', 4);
	insertElement(tempList, '!', 5);
	printList(tempList);

	// Step 3. Delete some characters (the first occurrence).
	deleteElement(tempList, 'e');
	deleteElement(tempList, 'a');
	deleteElement(tempList, 'o');
	printList(tempList);

	// Step 4. Insert to a given position.
	insertElement(tempList, 'o', 1);
	printList(tempList);
}// Of appendInsertDeleteTest

/**
 * Address test: beyond the book.
 */
void basicAddressTest(){
	DLNode tempNode1, tempNode2;

	tempNode1.data = 4;
	tempNode1.next = NULL;

	tempNode2.data = 6;
	tempNode2.next = NULL;

	printf("The first node: %d, %d, %d\r\n",
		&tempNode1, &tempNode1.data, &tempNode1.next);
	printf("The second node: %d, %d, %d\r\n",
		&tempNode2, &tempNode2.data, &tempNode2.next);

	tempNode1.next = &tempNode2;
}// Of basicAddressTest

/**
 * The entrance.
 */
void main(){
	insertDeleteTest();
	basicAddressTest();
}// Of main

对单链表代码稍作修改, 就可以获得双链表代码。

class Node {
	private:
		ElemType data;
		Node* next;
		Node* prev;
		
	public:
		virtual ~Node() {
				
		}
		
		Node();
		
		Node(ElemType data, Node* next, Node* prev) {
			this -> data = data;
			this -> next = next;	
			this -> prev = prev;
		}
		
		ElemType getData() {
			return this -> data;
		}
		
		void setData(ElemType elem) {
			this -> data = elem;
		}
		
		Node* getNext() {
			return this -> next;
		}
		
		void setNext(Node* node) {
			this -> next = node;
		}
		
		Node* getPrev() {
			return this -> prev;
		}
		
		void setPrev(Node* node) {
			this -> prev = node;
		}
};
双链表尾删

既然多了一个指针域, 我们考虑的问题也会增加, 首先, 有了前驱指针后, 我们可以在 O ( 1 ) O(1) O(1)时间完成对尾结点的删除:

template<class ElemType>
bool LinkedList<ElemType>::popBack() {
	if (this -> length == 1) {
		this -> head = this -> tail = nullptr;
		return 1;
	}
	
	if (this -> tail == nullptr) {
		return 0;
	}
	
	Node* tailPrev = this -> tail -> getPrev();
	tailPrev -> setNext(nullptr);
	delete this -> tail;
	this -> tail = tailPrev;
	return 1;
}
双链表插入

同时, 在插入时, 我们考虑的事情也要增加, 前后指针都要处理好
在这里插入图片描述

template<class ElemType>
bool LinkedList<ElemType>::insertElement(int paraPosition, ElemType elem) {
	if (paraPosition > this -> getLength() + 1) {
		return 0;	
	}
	
	if (paraPosition == 1) {
		return this -> pushHead(elem);
	}
	
	Node* temp = this -> head;
	int recordIndex = 1;
	
	while (recordIndex < paraPosition - 1) {
		temp = temp -> getNext();
		recordIndex++;	
	}
	
	Node* newNode = new Node(elem, temp -> getNext(), temp);
	temp -> setNext(newNode);
	this -> length++;
	return 1;
}

我们也可以, 根据要插入的位置和总长度的大小, 判断是从头还是尾开始遍历, 以节省时间, 在查找元素时, 也可以判断相对距离, 来提高效率。

双链表删除

在删除时, 也需要重新调整前驱指针的位置
在这里插入图片描述

template<class ElemType>
bool LinkedList<ElemType>::deleteElementByParaPosition(int paraPosition) {
	if (paraPosition > this -> getLength()) {
		return 0;
	}
	
	if (paraPosition == 1) {
		return popHead();
	}
	
	int recordIndex = 1;
	Node* temp = this -> head;
	
	while (recordIndex < paraPosition - 1) {
		temp = temp -> getNext();	
		recordIndex++;
	}
	
	Node* recorder = temp -> getNext();
	temp -> setNext(recorder -> getNext());
	recorder -> getNext() -> setPrev(temp);
	delete recorder;
	return 1;
}

单双链表各自的优点和缺点

对于双链表来说, 双链表每个结点拥有前驱指针, 那么它就可以花费更少的时间来处理问题, 提高程序运行效率, 比如说单链表消耗 O ( n ) O(n) O(n)来删除尾结点, 双链表可以在常数时间内完成, 而插入和删除, 也可以只遍历表长的一半, 缺点是, 和单链表比起来, 双链表构建更复杂, 且每个结点需要额外维护一个前驱指针, 也花费了内存。 不过现在人们更关于程序执行效率, 对于内存占用不太关心, 因此双链表综合更为优秀。

正如人生一般, 欲收获就得付出代价。

循环链表

现在我们都很年轻, 但是也许过几十年后, 我们就会怀念以前了。

对于单链表, 每个结点只存储了向后的指针, 到了尾就无法找到尾结点的后继, 正如无法回到从前一般。
将单链表中终端结点的指针域指向头结点, 就可以让单链表形成一个环, 称为循环链表。
如果链表是循环的, 那么我们可以从任何一结点出发, 按一个方向访问到所有结点。

this -> tail -> setNext(this -> head);

静态链表

老师版本:

#include <stdio.h>
#include <malloc.h>

#define DEFAULT_SIZE 5

typedef struct StaticLinkedNode{
	char data;

	int next;
} *NodePtr;

typedef struct StaticLinkedList{
	NodePtr nodes;
	int* used;
} *ListPtr;

/**
 * Initialize the list with a header.
 * @return The pointer to the header.
 */
ListPtr initLinkedList(){
	// The pointer to the whole list space.
	ListPtr tempPtr = (ListPtr)malloc(sizeof(StaticLinkedList));

	// Allocate total space.
	tempPtr->nodes = (NodePtr)malloc(sizeof(struct StaticLinkedNode) * DEFAULT_SIZE);
	tempPtr->used = (int*)malloc(sizeof(int) * DEFAULT_SIZE);

	// The first node is the header.
	tempPtr->nodes[0].data = '\0';
	tempPtr->nodes[0].next = -1;

	// Only the first node is used.
	tempPtr->used[0] = 1;
	for (int i = 1; i < DEFAULT_SIZE; i ++){
		tempPtr->used[i] = 0;
	}// Of for i

	return tempPtr;
}// Of initLinkedList

/**
 * Print the list.
 * @param paraListPtr The pointer to the list.
 */
void printList(ListPtr paraListPtr){
	int p = 0;
	while (p != -1) {
		printf("%c", paraListPtr->nodes[p].data);
		p = paraListPtr->nodes[p].next;
	}// Of while
	printf("\r\n");
}// Of printList

/**
 * Insert an element to the given position.
 * @param paraListPtr The position of the list.
 * @param paraChar The given char.
 * @param paraPosition The given position.
 */
void insertElement(ListPtr paraListPtr, char paraChar, int paraPosition){
	int p, q, i;

	// Step 1. Search to the position.
	p = 0;
	for (i = 0; i < paraPosition; i ++) {
		p = paraListPtr->nodes[p].next;
		if (p == -1) {
			printf("The position %d is beyond the scope of the list.\r\n", paraPosition);
			return;
		}// Of if
	} // Of for i

	// Step 2. Construct a new node.
	for (i = 1; i < DEFAULT_SIZE; i ++){
		if (paraListPtr->used[i] == 0){
			// This is identical to malloc.
			printf("Space at %d allocated.\r\n", i);
			paraListPtr->used[i] = 1;
			q = i;
			break;
		}// Of if
	}// Of for i
	if (i == DEFAULT_SIZE){
		printf("No space.\r\n");
		return;
	}// Of if

	paraListPtr->nodes[q].data = paraChar;

	// Step 3. Now link.
	printf("linking\r\n");
	paraListPtr->nodes[q].next = paraListPtr->nodes[p].next;
	paraListPtr->nodes[p].next = q;
}// Of insertElement

/**
 * Delete an element from the list.
 * @param paraHeader The header of the list.
 * @param paraChar The given char.
 */
void deleteElement(ListPtr paraListPtr, char paraChar){
	int p, q;
	p = 0;
	while ((paraListPtr->nodes[p].next != -1) && (paraListPtr->nodes[paraListPtr->nodes[p].next].data != paraChar)){
		p = paraListPtr->nodes[p].next;
	}// Of while

	if (paraListPtr->nodes[p].next == -1) {
		printf("Cannot delete %c\r\n", paraChar);
		return;
	}// Of if

	q = paraListPtr->nodes[p].next;
	paraListPtr->nodes[p].next = paraListPtr->nodes[paraListPtr->nodes[p].next].next;
	
	// This statement is identical to free(q)
	paraListPtr->used[q] = 0;
}// Of deleteElement

/**
 * Unit test.
 */
void appendInsertDeleteTest(){
	// Step 1. Initialize an empty list.
	ListPtr tempList = initLinkedList();
	printList(tempList);

	// Step 2. Add some characters.
	insertElement(tempList, 'H', 0);
	insertElement(tempList, 'e', 1);
	insertElement(tempList, 'l', 2);
	insertElement(tempList, 'l', 3);
	insertElement(tempList, 'o', 4);
	printList(tempList);

	// Step 3. Delete some characters (the first occurrence).
	printf("Deleting 'e'.\r\n");
	deleteElement(tempList, 'e');
	printf("Deleting 'a'.\r\n");
	deleteElement(tempList, 'a');
	printf("Deleting 'o'.\r\n");
	deleteElement(tempList, 'o');
	printList(tempList);

	insertElement(tempList, 'x', 1);
	printList(tempList);
}// Of appendInsertDeleteTest

/**
 * The entrance.
 */
void main(){
	appendInsertDeleteTest();
}// Of main

在此我使用另一种数组模拟的方式来实现静态链表。

有些编程语言没有指针操作, 也没有像java一般的引用操作, 在这些语言上无法实现动态分配结点的链表, 于是出现了用数组来模拟链表。数组可以模拟单链表, 也可以模拟双链表。
模拟单链表, 需要三个数组和一个游标值以及一个记录头结点位置的值, 也就是head, idx, e[], ne[], idx是游标, e[i]存储i位置结点的值, 而ne[i]存储i结点的后继。
在此没有使用used数组来标定某个位置是否被有效结点占用, 只是使用idx游标, 一直向后移动, 移动到的必然是可用位置。 如果有需求, 可以在游标到达末尾后, 再将游标从0开始, 并且用next来代替used数组, 这样的目的主要是为了保证 o ( 1 ) o(1) o(1)的头插入。
在此使用next数组的值等于-1来说明这是尾结点。 可以这么做的原因是, 初始化head的值等于-1

初始化
void init() {
    head = -1;
    idx = 0;
}
头插

当头插时, 我们直接向idx位置填入数据, 并且让idx位置的后继是head位置, 再重新赋值head

void pushBack(int x) {
    e[idx] = x;
    ne[idx] = head;
    head = idx++;
}
插入

插入时, 我们只需要让需插入位置的后继是新插入的结点, 然后新插入结点的后继是原结点的后继, 这点逻辑上跟动态链表相同。

void insert(int k, int x) {
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx++;
}
删除

逻辑也与动态链表相同

void popRandom(int k) {
    ne[k] = ne[ne[k]];
}
遍历输出
int temp = head;

while (temp != -1) {
	cout << e[temp] << " ";
	temp = ne[temp];
}

静态链表在逻辑上模拟了链表, 但是仍然占用固定位置的空间, 而不是根据结点的数量而占据不同的空间大小。
在实际使用中, 如果使用静态链表, 则静态链表的空间最大值应该大于数据量最大值, 因为我们不知道什么时候才会删除。

静态双链表

逻辑跟双链表大体相同, 在此不做赘述

int quize[MaxN] , front[MaxN] , behind[MaxN] , idx;
void init() {
    behind[0] = 1;
    front[1] = 0;
    idx = 2;
} 
void pushFront(int x) {
    front[idx] = 0;
    behind[idx] = behind[0];
    behind[0] = idx;
    front[behind[idx]] = idx;
    quize[idx++] = x;
}
void pushBack(int x) {
    behind[idx] = 1;
    front[idx] = front[1];
    front[1] = idx;
    behind[front[idx]] = idx;
    quize[idx++] = x;
}
void pop(int k) {
    front[behind[k]] = front[k];
    behind[front[k]] = behind[k];
}
void insertLeft(int k , int x) {
    front[idx] = front[k];
    behind[front[k]] = idx;
    front[k] = idx;
    behind[idx] = k;
    quize[idx++] = x;
}
void insertRight(int k , int x) {
    behind[idx] = behind[k];
    front[behind[k]] = idx;
    front[idx] = k;
    behind[k] = idx;
    quize[idx++] = x;
}

总体来说, 线性表分为顺序表和链表, 这两种类型各有优劣。
当我们需要的内存空间比较灵活时或者我们需要进行大量插入删除操作, 使用链表。
当我们需要快速存取元素, 用顺序表。

感悟

不得不说现在的竞争实在是非常激烈, 无论各种方面, 各个领域, 都不约而同的"内卷"了起来, 在市场经济下, 我们可以从任何位置开始起步, 但是一不小心, 自己的位置也会被他人插足, 这也像是线性表的存储结构, 任何位置都可以插入和删除。
在内卷环境下, 也许有些人提不起斗志, 实则是把学习看作是吃苦, 实际上学习也许是一件很令人满足的事, 因为我们可以做到自己做不到的事, 做到自己想做到的事, 可以看到自己的成长, 也可以获得他人的尊敬甚至是仰慕。 一旦我们感悟到了学习上的乐趣, 也许就不会再去追求游戏上的乐趣, 自然也不会因为内卷严重而丧失斗志, 反正他们卷归他们卷, 我们只需要做好自己喜欢做的事就行。 希望我们都可以不为了学位或者是金钱学习编程, 而是发自内心的喜爱编程, 感受编程带给我们的乐趣和成长。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值