单链表是一个非常常用的数据结构,不得不说,它非常简单,也不得不说,还是有很多细节需要注意的。
一般说来,单链表的单个结点都会被定义为一个struct
,结构中则主要包含两个成员,用于存储结点键值的data
,和用于指向链表中下一个结点的指针next
。为了便于操作,一个链表通常会包含一个头结点,头结点中存储的不是用户数据,而是一些另外的信息,如链表中结点的个数等,头结点的next
指针域指向链表中的第一个有效结点(首结点)。头结点的维护,对于某些信息的获取,及结点的插入和删除都带来了极大的便利。链表头结点指针head
是每个链表中都会有的,大多数链表也会维护一个尾结点指针tail
,其目的当然也是方便操作。
基本操作
单链表的基本操作包括创建链表、插入结点、删除结点、获取链表长度(不包括头结点)、逆置链表、链表排序、清空链表、销毁链表等。
创建链表的主要目的是创建头结点,并使得头指针head
指向头结点,同时将头结点数据域中存储的链表长度初始化为0。
插入节点都在链表的头部进行,因为这样操作很方便,当然,如果维护了尾指针tail
,也可以从尾部插入结点。重点是要方便地确定新结点的插入位置。
删除结点时,通过参数idx
来确定被删除结点的位置,有效结点的索引从0开始编号。
直接返回头结点中数据域的值就可以获取链表长度。
逆置链表时,需要借助三个指针p1
、p2
和p3
,依次指向原链表中连续的三个结点,其中p1和p2
的指向关系需要修改,p3
主要用于保存后续结点的位置。如下图所示,p1
、p2
、p3
位置确定后,让p2->next
指向p1
,然后依次将p1
、p2
、p3
后移(在图中的位置),重新修改p1
和p2
的指向关系,当p2
为空时,表明链表已经遍历完毕,相应的位置也已修改完毕,此时修改链表头即可。整个过程只需遍历一次链表即可。
链表排序相对来说是一个比较复杂的过程,首先考虑到对于单链表,访问某一结点的后续结点是比较容易的,但是要访问某一结点的前序结点,却似乎只能通过遍历的方式来完成;此外,在结点需要重排序时,如果直接通过修改指针来改变整个结点的位置,一方面操作复杂,需要考虑各种边界条件,另一方面指针操作稍有不当就会发生各种错误。综合考虑单链表的上述特征,觉得冒泡排序似乎是比较合适的。
清空链表的操作,只是将链表中的有效结点全部删除,只保留头结点,也就是,链表还存在,只是它是空的。删除每个结点的同时,要注意释放掉结点所占用的内存空间。
销毁链表操作相对于清空链表的操作更为彻底,除了要删除链表中的有效结点,还要将头结点也删除。也就是最后,这个链表已经不存在了。
代码实现
基于上述分析,最终代码实现如下:
LinkedList.h
#ifndef _LIST_H_
#define _LIST_H_
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <iomanip>
using namespace std;
typedef struct Node {
int data;
struct Node * next;
}node;
node * createList();
int getLength(node * head);
bool insertNode(node * head, int e);
bool deleteNode(node * head, int idx);
void reverseList(node * head);
void sortList(node * head);
void emptyList(node * head);
void destroyList(node * head);
void printList(node * head);
bool getMidNode(node * head, int &mid);
#endif
LinkedList.cpp
#include "LinkedList.h"
node* createList()
{
node *head;
head = (node *)malloc(sizeof(node));
if(!head){
return NULL;
}
head->data = 0;
head->next = NULL;
return head;
}
int getLength(node * head)
{
if(head == NULL){
return -1;
}
return head->data;
}
bool insertNode(node * head, int e)
{
node * newNode;
if(!head){
return false;
}
newNode = (node *)malloc(sizeof(node));
if(!newNode){
return false;
}
newNode->data = e;
newNode->next = head->next;
head->next = newNode;
head->data ++;
return true;
}
bool deleteNode(node * head, int idx)
{
int i;
node * temp,*p;
int length = getLength(head);
if(!head || idx>=length){
return false;
}
p = head;
temp = p->next;
for(i=0;i<length;i++){
if(i == idx){
p->next = temp->next;
temp->next = NULL;
free(temp);
head->data --;
break;
}
p = temp;
temp = p->next;
}
return true;
}
void reverseList(node * head)
{
node * p1,*p2,*p3;
if(!head)
{
return;
}
if(getLength(head) <= 1){
return;
}
p1 = head->next;
p2 = p1->next;
p1->next = NULL;
while(p2){
p3 = p2->next;
p2->next = p1;
p1 = p2;
p2 = p3;
}
head->next = p1;
}
void sortList(node * head)
{
int i,j,e;
int length;
node * p1,* p2;
if(!head){
return;
}
length = getLength(head);
if(length<=1){
return ;
}
for(i=1;i<length;i++){
p1 = head->next;
p2 = p1->next;
for(j=length-i;j>=1;j--){
if(p1->data > p2->data){
e = p1->data;
p1->data = p2->data;
p2->data = e;
}
p1 = p2;
p2 = p1->next;
}
}
}
void emptyList(node * head)
{
node * temp;
if(!head){
return ;
}
if(getLength(head) == 0){
return ;
}
temp = head->next;
while(temp){
head->next = temp->next;
temp->next = NULL;
free(temp);
temp = head->next;
}
head->data = 0;
}
void destroyList(node * head)
{
if(!head){
return;
}
emptyList(head);
free(head);
}
void printList(node* head)
{
node *temp;
if (!head){
return;
}
temp = head->next;
while(temp){
cout << left << setw(5) << temp->data ;
temp = temp->next;
}
cout << endl;
}
bool getMidNode(node * head, int & mid)
{
node * p,*q;
if(!head){
return false;
}
if(getLength(head)==0){
return false;
}
p = q = head;
while(q){
q = q->next;
if(!q){
break;
}
p = p->next;
q = q->next;
}
mid = p->data;
return true;
}
test.cpp
#include "LinkedList.h"
int main()
{
int mid;
node * head;
int rawData[] = {1,3, 2, 5, 7, 9, 6, 0};
head = createList();
if(head == NULL){
cout << "can't create list!" << endl;
return -1;
}
for(int i=0;i<sizeof(rawData)/sizeof(int);i++){
insertNode(head,rawData[i]);
}
printList(head);
cout << "-----" << endl;
if(getMidNode(head,mid)){
cout << "Mid Node : " << mid << endl;
}
cout << "-----" << endl;
reverseList(head);
printList(head);
insertNode(head,10);
printList(head);
cout << "-----" << endl;
if(getMidNode(head,mid)){
cout << "Mid Node : " << mid << endl;
}
cout << "-----" << endl;
deleteNode(head,0);
printList(head);
sortList(head);
printList(head);
destroyList(head);
return 0;
}
运行结果
本来是打算封装一个类来实现的,但是在实现的过程中,因为涉及到指针操作以及实例的析构,遇到了各种问题。果然,一旦涉及到内存的分配和释放,就得格外留心,暂时先这样吧,链表类的实现,等稍后问题都解决了,再补上吧。
餐后甜品
如果给你一个单链表,但是不告诉你链表长度是多少(你也不能直接获取到这个长度),你怎么能够只遍历一次,就取到中间结点的值呢?
问题的解法很巧妙(起码我这么认为),就是利用两个结点指针p
和q
,初始时刻,它们都指向头结点,然后开始遍历,p
结点每次向后遍历的跨度是一个结点,而q
结点每次向后遍历的跨度是两个结点,那么当q
结点为空时,p
结点所对应的就是链表的中间结点了。对应的代码实现,就是getMidNode()
函数了。
小贴士
还是那句话,操作指针时要格外留心。一旦决定要使用某一个指针,一定要确保该指针被初始化了,即使该指针只是作为一个参数传入某一个函数也不能例外;当试图通过指针获取某些内容时,一定要先确保该指针不是空指针,否则编译不会出错,但是运行时就会出现很大的错误了。
一定要注意函数参数的传递方式,当你要试图通过参数获取某一个值时,采用引用传递是一个不错的选择;地址传递也是可行的,只是操作要格外小心;值传递就不要想了,因为这样你是不可能通过这个参数获取到任何东西的。