单链表是一种常见的线性数据结构,由一系列结点组成。每个结点由包含数据元素的数据域和保存下一个结点地址的指针域构成。
举个例子吧,假如现在你要去找你的辅导员办件事,首先你去了辅导员办公室(第一个结点),办公室的另外一个老师告诉你,你要找的人不在这,他在食堂干饭;于是你去食堂,食堂的打饭阿姨告诉你,你要找的人不在这(已经吃完了),他在图书馆看书;于是你去了图书馆,你找到他了!
这个例子中办公室、食堂、图书馆都可以看作一个链表中的结点,每个结点都有人(指针域)告诉你下一个应该去哪里。这一串就是一个单链表。
相比于数组,链表的大小可以根据需要动态调整(调整指针里保存的地址,例如在食堂的时候告诉你下一个是操场,你就会去操场而不是图书馆),并且插入和删除操作的时间复杂度较低(因为不需要大量移动数据)。
直接上结论,构建链表的步骤如下:
结点由数据域和指针域构成,这肯定是要我们自己定义数据类型了:
typedef struct link {
int elem;//代表数据
struct link* next;//代表指针
}Link;
注意: 这里使用typedef重命名类型名称为Link
,而不必每次都写struct link
,方便以后书写。
结点的指针指向下一个结点的地址,所以要和结点类型保持一致。就好像指向整型int
的指针类型是int*
,这里指向struct link
类型结点的指针类型是 struct link*
。
现在就是要创建头指针、头结点,并让头指针指向头结点。因为以后学习链表创建链表肯定必不可少,所以封装一个创建链表的函数,它返回一个链表的头指针。这里创建一个0,1,2,3,4的链表
Link* initLink() {
//创建头指针
Link* head;
//创建结点
Link* node = new Link;
node->elem = 0;
node->next = NULL;
//头指针指向第一个结点
head = node;
for (int i = 1; i <= 4; i++) {
Link* newNode = new Link;
newNode->elem = i;
newNode->next = NULL;
//让当前结点的指针指向下一个结点
node->next = newNode;
//更新node
node = node->next;
}
//返回头指针
return head;
}
注意:不能把Link* node = new Link;
写成Link* node;
原因:因为 node 是一个指向 Link 类型结点的指针,逻辑上它应该指向一个实际的结点对象。而在没有为 node 分配内存空间时,试图访问 node 的成员变量 elem 和 next 会导致未定义行为。
解决办法:使用 new 关键字来动态分配内存,即动态创建一个 Link 类型的结点对象。
当执行到head = node;
时,第一个结点相当于有两个指针指向,由于单链表只能一直向后访问,所以头结点是关键。以后的操作由node
完成,head
始终指向头结点。最后创建完如下
增加结点: 在链表中增加结点可以分为三种情况:
//在链表某一位置插入新结点
Link* insertNewNode(Link* head,Link* newNode, int position) {
if (head == NULL) {
return newNode;
}
//getLinkLength()用于获取某一链表的结点数(长度)
int size = getLinkLength(head);
if (position < 1 || position > size + 1) {
cout << "插入的位置错误" << endl;
return head;
}
//插入为头结点
if (position == 1) {
head->next = newNode->next;//新结点的下一个是头结点
head = newNode; //更新头指针的位置,指向新的第一个结点
return head;
}
//其他位置
Link* p = new Link;
p = head;
//找到插入位置的前驱结点
for (int i = 1; i < position - 1; i++) {
p = p->next;
}
newNode->next = p->next;
p->next = newNode;
return head;
}
删除结点: 在链表中删除结点也可以分为三种情况:
最后两种也可以和为一种:
//删除链表中某一位置的结点
Link* deleteNode(Link* head, int position) {
if (head == NULL) {
return head;
}
int size = getLinkLength(head);
if (position < 1 || position > size + 1) {
cout << "删除的位置错误" << endl;
return head;
}
if (position == 1) {
head = head->next;
return head;
}else {
//找到要删除结点的前驱结点
Link* p = new Link;
p = head;
for (int i = 1; i < position - 1; i++) {
p = p->next;
}
p->next = p->next->next;
return head;
}
}
当在链表的首部、中部或尾部增加或删除一个结点时,可能会遇到以下问题:
1. 在中部、尾部增加/删除结点:
例如上文增加一个结点时,我们会用Link* p = new Link;
创建一个新的指针p
来查找,这样就能保持头指针不变。
2. 在尾部增加/删除结点: