上次说到,具有线性结构的数据结构又被称为线性表(Linear list),再往下分,就是以数组为代表的连续存储线性表以及以链表为代表的链式存储线性表了。
基础结构
链表结构是由一个或多个节点组成的,节点又是由数据域和指针域组成的,现在介绍相关概念。
数据域:用来存储数据元素,“域”就是“成员”、“字段”。
指针域:用来指向下一个节点。
头指针:永远指向链表中的第一个节点,存储第一个节点的地址。是访问链表的唯一路径,没有头指针就访问不了链表。其也是链表结构的标识。
哨兵节点:是为了操作与方便,而放在第一个存储数据元素的节点之前的节点,可有可无,其数据域一般没有意义,也可能会存放链表的长度。主要用于对插入删第一元素时能够统一操作逻辑。
(这里笔者查阅了一些资料,发现在一些书籍中提到的有上述作用的“头节点”其实是“哨兵式头节点”,而在《算法导论》这种更专业的书籍中,哨兵节点与头节点是有明确区分的,哨兵节点 Sentinel Node 才是起上述作用的辅助节点,用于简化边界条件,而头节点 Head Node 就只是指链表中第一个节点罢了,可以是没有实际数据的辅助节点,也可以是普通的节点。在实际运用时,一定要谨慎区分。)
单链表:每个节点只有一个指针指向下一个节点,最后一个节点指向NULL。
双链表:每个节点有两个指针,分别指向前一个节点和后一个节点。
循环链表:尾节点的指针指向头节点,形成环状结构。
//C++节点类实现
struct Node {//数据域int num;//指针域Node* next;//重载各种情况下的构造函数,用于初始化成员Node() : num(0), next(nullptr) {}Node(int x) : num(x), next(nullptr) {}Node(int x, Node* next) : num(x), next(next) {}
};
基本操作
下面用C++实现链表中的各种操作。
创建链表:第一个节点创建好后,不断让尾部节点的指针存放新节点的地址。
//读取数据
Node* create(vector<int> a) {//头指针,指向第一个节点Node* head = nullptr;//尾部指针,用于插入元素Node* tail = nullptr;//逐个插入for (int i = 0; i < a.size(); i++) {//将节点初始化Node* newNode = new Node(a[i]);//分清楚是第一个节点还是其他节点if (head == nullptr) {head = newNode;tail = newNode;}else {tail->next = newNode;tail = newNode;}}//返回头指针return head;
}
遍历链表:创建一个不断移动的指针,通过这个指针遍历元素。
//输入头指针
void traversal(Node *head) {//创建移动指针Node* current = head;while (current != nullptr) {cout << current->num << " ";current = current->next;}
}
释放链表:同遍历一样,创建一个不断移动的指针,从头节点开始依次释放。
void free(Node* head) {//创建移动指针Node* current = head;while (current != nullptr) {//先提前保存下一个节点Node* next = current->next;//释放当前节点delete current;//回到刚刚保存的节点current = next;}
}
查找节点:遍历时一一对比即可。
//输入头指针与要查找的值
Node* search(Node* head, int value) {//创建移动指针Node* current = head;while (current != nullptr) {if (current->num == value) {return current;}current = current->next;}return nullptr;
}
插入节点:分为头部插入和尾部插入。当然也可以插在指定元素后面或者之前。
//头插
Node* insert(Node* head, int num) {Node* newNode = new Node(num, head);return newNode;
}
//尾插
Node* insert(Node* head, int num) {Node* newNode = new Node(num);Node* current = head;while (current->next != nullptr) {current = current->next;}current->next = newNode;return head;
}
删除节点:在对头部进行删除时,需要先让头指针指向头节点的下一个节点。当然也可以删除指定元素,这很像查找操作。
//删除头节点
Node* DeleteFirst(Node* head) {if (head == nullptr) {return nullptr;}Node* newHead = head->next;delete head;return newHead;
}
交换节点:交换也算是排序的核心,掌握交换后,可以用于插入排序、冒泡排序等排序算法。(不管排序常用归并。)在一般情况下进行值交换即可,效率更高。
Node* swapNode(Node* head, int a, int b) {Node* previous1 = nullptr, *current1 = head;Node* previous2 = nullptr, *current2 = head;while (current1 != nullptr) {if (current1->num == a) {break;}previous1 = current1;current1 = current1->next;}while (current2 != nullptr) {if (current2->num == b) {break;}previous2 = current2;current2 = current2->next;}//检测是否为空if (current1 == nullptr || current2 == nullptr) {return head;}//暂时保存Node* temp = current2->next;if (previous1 != nullptr) {previous1->next = current2;}else {head = current2;}current2->next = current1->next;if(previous2 != nullptr){previous2->next = current1;}else {head = current1;}current1->next = temp;return head;
}
常用技巧
这里总结了一些在处理链表结构时常常会用到的技巧方法。
反转链表:原链表的第一个节点是新链表的最后一个,然后不断头插即可。
Node* reverse(Node* head) {Node* newHead = nullptr;Node* current = head;while (current != nullptr) {Node* next = current->next;//不断地加到头部current->next = newHead;newHead = current;//继续向后遍历current = next;}return newHead;
}
快慢指针检测链表为环:要知道慢指针必须得走动用来防止环入口不是头节点的情况。
bool hasCycle(Node* head) {if (head == NULL || head->next == NULL) {return false;}//一开始就别在一块,方便编写循环条件,当然可以使用do while避免Node* slow = head;Node* fast = head->next;while (slow != fast) {if (fast == NULL || fast->next == NULL) {return false;}slow = slow->next;fast = fast->next->next;}return true;
}
快慢指针寻找中间节点:前面指针的移动速度是后面指针的两倍。在归并排序里会用到。
Node* middleNode(Node* head) {Node* slow = head;Node* fast = head;while (fast != NULL && fast->next != NULL) {slow = slow->next;fast = fast->next->next;}return slow;
}
C++STL库提供的链表:分别是双向链表<list>以及单向链表<forward_list>。同vector一样支持begin()、end()等迭代器以及使用迭代器的范围遍历操作。下面介绍一下list,因为二者操作基本相同,只不过forward_list没有尾部插入等反向操作。
list<int> myList;myList.push_back(10);//尾部插入myList.push_front(5);//头部插入myList.pop_back();//尾部删除myList.pop_front();//头部删除myList.erase(it);// 删除迭代器指向的元素myList.merge(otherList);//能够将有序链表合并
小结
关于链表就说这么多了,我们下次再见!
如有补充纠正欢迎留言。