单链表在数据结构中是很重要的一种线性结构,它是单向的,有着非常广泛的应用领域;虽然现在很多语言中都有封装好的链表类型可以直接使用,但是自己能写一个链表并实现基本操作是至关重要的;
接下来我将用代码展示单链表的创建和一些基本操作;
注:以下代码仅供参考,并不一定是最优代码,只是想让各位了解单链表如何进行的一些基本操作;
单链表就是由一个一个节点组成,这个节点由一个数据域和指针域组成;
如图:
所以,我们需要先创建节点结构,然后才能依次组成单链表;
注:以下链表的数据域的数据类型都是int类型,实际情况可以修改为任意数据类型;
这里使用结构体来创建,代码如下:
// 单链表节点结构
typedef struct Node {
int data; // 数据域
struct Node* next; // 指针域
}NODE;
可以看到,指针域指向的其实就是该节点本身的数据类型,所以这一点一定要注意!
有了基本组成单元,那么就要创建链表了,这里我分为两个函数来实现:
void initList(NODE*& head); // 初始化链表
void creatList(NODE*& head); // 创建一个链表
初始化链表很简单,直接看代码:
// 初始化链表
void initList(NODE*& head) {
try {
head = new NODE;
}
catch (bad_alloc& e) {
cout << "内存分配失败!!" << endl;
cout << e.what() << endl; // 输出异常信息
} // 捕获异常
head->data = 0;
head->next = NULL;
return;
}
这里还有一点值得一提:
当链表内存分配失败时,我是用的是try…catch捕获的异常,这个使用于现在的大部分新版的编译器,因为新版编译器在内存分配失败的情况下将不再会返回NULL;老版的编译器入VC++6.0等就会返回NULL,所以一定要注意对内存分配失败的处理;
接下来就是创建一个单链表了,这里使用的是尾插法;
为什么使用尾插法呢?因为头插法输出的链表和输入的顺序是相反的,所以最好使用尾插法来创建链表;
这里创建的链表也需要注意:
该链表创建时会有一个没有什么实际意义的头节点,它的数据域存放的是链表的长度,当然创建头节点的目的也是为了方便对链表的操作;
代码如下:
void creatList(NODE*& head) {
// 1,确定创建链表长度
int len;
cout << "请输入创建链表长度:" << endl;
cin >> len;
// 链表长度不应该为0和负数
if (len <= 0) {
cout << "创建链表长度不能是0或负数" << endl;
return;
}
head->data = len; // 头节点数据域存放链表长度
// 2,创建一个尾节点
NODE* tail = head; // 定义一个节点为尾节点,指向头节点,它将代替头节点移动
tail->next = NULL;
// 3,循环创建新的节点
for (int i = 0; i < len; ++i) {
cout << "请输入第" << i + 1 << "个数据" << endl;
int val;
cin >> val;
NODE* newNode = NULL;// 创建一个新节点作为临时节点
try {
newNode = new NODE;
}
catch (bad_alloc& e) {
cout << "内存分配失败!!" << endl;
cout << e.what() << endl;
} // 捕获异常
newNode->data = val; // 新节点数据域赋值
tail->next = newNode; // 将新节点挂在尾节点后面
newNode->next = NULL; // 新节点指针域为空
tail = newNode; // 尾节点为新的节点
}
return;
}
这一步的操作一定要学会,因为只有创建出来链表后你才能对链表进行其他操作;
下面的操作是遍历链表并输出和返回链表的长度;
为什么把这两个函数放一起?因为它们的操作可以说是一模一样,只是有很小的改动;
当然对链表遍历也是非常简单,所以不需要有太大的心理负担;
遍历输出链表代码如下:
void traverseList(NODE* head) {
NODE* p = head->next; // 临时节点p指向头节点的下一个节点
while (p) {
cout << p->data << " ";
p = p->next; // p移向下一个节点
}
cout << endl;
return;
}
虽然简单,但是还是需要注意一点:
临时节点不要忘记,因为头节点是没有什么实际意义的节点,所以输出头节点并没有什么意义;
获取链表长度代码如下:
int listLength(NODE* head) {
NODE* p = head->next; // 临时节点p指向头节点的下一个节点
int len = 0; // 链表长度
while (p) {
++len; // 长度加一
p = p->next; // p移向下一个节点
}
return len;
}
是不是很简单,只是只需要修改关键的一句代码即可求得链表长度;
有时我们可能会遇到一些情况需要对链表进行排序,链表是线性存储结构,那么该如何排序呢?
其实也很简单,只需要将数据域的内容进行交换排序即可,也就是只对数据域进行操作,并不改变链表结构;
这里是顺序排序;
代码如下:
void sortList(NODE*& head) {
int t;
NODE* p;
NODE* q;
for (p = head->next; p != NULL; p = p->next) {
for (q = p->next; q != NULL; q = q->next) {
// 交换数据域的内容
if (p->data > q->data) {
t = p->data;
p->data = q->data;
q->data = t;
}
}
}
}
这个排序算法并不是最优的,但是非常好理解,所以先学会一种方法再去突破吧!
单链表和数组都是线性存储结构,数组的优点是:可以实现快速查询;链表的的优点是:可以快速的实现插入和删除操作;
所以,插入和删除操作在单链表中是非常非常非常重要的!!
对于链表的插入和删除操作,只需要记住一点:插入/删除哪个位置,一定要找到该位置的前一个位置;
下面就来看一下代码:
void insertListByPostion(NODE*& head, int data, int pos) {
int i = 1;
NODE* p = head;
while (p && i < pos) {
++i;
p = p->next;
}
if (!p || i > pos) {
cout << "插入位置不存在!!" << endl;
return;
}
NODE* newNode = NULL;
try {
newNode = new NODE;
}
catch (bad_alloc& e) {
cout << "内存分配失败!!" << endl;
cout << e.what() << endl;
} // 捕获异常
newNode->data = data;
newNode->next = p->next;
p->next = newNode;
head->data++;
cout << "插入成功!!" << endl;
}
需要注意:插入节点位置必须存在,即首部尾部和中间,所以前面需要先判断插入位置是否存在;
删除操作更简单,如图:
同样描述一下该图:
是不是很简单;
来看一下代码如何实现:
void deleteListByPostion(NODE*& head, int pos) {
int i = 1;
NODE* p = head;
while (p->next && i < pos) {
p = p->next;
++i;
}
if (!p->next || i > pos) {
cout << "删除位置不存在!!" << endl;
return;
}
NODE* q = p->next;
p->next = q->next;
cout << "删除成功!!删除元素为:" << q->data << endl;
delete q;
head->data--;
}
同样需要注意:删除节点位置必须存在,所以需要先判断插入位置是否存在,因为删除只能删除节点只能删除存在的节点,所以判断条件和插入节点有些不同,不理解可以画个图细细品味;
这个操作其实已经不算是单链表的基本操作了,它是将两个顺序的链表合并为一个顺序的链表,并且使用O(1)的空间复杂度,既不能使用额外空间,但是考研时经常会出现,所以就来实现一下;
其实力扣上有原题,可以看看我的这篇文章:21. 合并两个有序链表(C语言),这里就不再写解析了;
代码如下:
void mergeTwoLists(NODE*& l1, NODE*& l2, NODE*& list) {
list->data = l1->data + l2->data; // 头节点数据域为两个链表个数之和
NODE* p = list;
NODE* list1 = l1->next; // list1为l1头节点下一个节点
NODE* list2 = l2->next; // list2为l2头节点下一个节点
// 要对没有头节点的链表进行操作,一定不能带上头节点进行比较
while (list1 && list2) {
if (list1->data < list2->data) {
p->next = list1;
list1 = list1->next;
}
else {
p->next = list2;
list2 = list2->next;
}
p = p->next;
}
p->next = list1 ? list1 : list2;
return;
}
其实这里还是需要注意一点:
力扣上的测试链表是没有头指针的,所以我们的链表不能直接去使用力扣上的代码,需要进行一些小改动,但是基本的算法思想还是一样的;
这个操作是将链表逆置,且同样不能使用额外内存空间;这个力扣上也有原题,解析可以看看我的这篇文章:剑指 Offer 24. 反转链表(C语言)
这个操作其实非常简单,就是一个双指针操作;
代码如下:
void reverseList(NODE*& head, NODE*& list) {
// 双指针
list = head; // 先把头节点连接到新的链表上
NODE* fast = head->next;
NODE* slow = NULL;
while (fast) {
NODE* node = fast->next;
fast->next = slow;
slow = fast;
fast = node;
}
list->next = slow;
}
同样需要注意:
力扣上的测试链表是没有头指针的,所以我们的链表不能直接去使用力扣上的代码,需要进行一些小改动,但是基本的算法思想还是一样的;
链表的操作其实是非常多的,这里只是为你开一个头,不管你是刚学习数据结构的小白,还是有经验的老手,都希望这篇文章可以给你带来一些启发;
数据结构的学习并不一定会让你瞬间感觉到编程能力的提升,这也是很多人学完数据结构感觉没什么用的原因;但是正是数据结构描述了我们生活抽象的事物,让它们变成了代码展示出来,数据结构在你的编程路上是要一直学习和理解的,只有对底层有更深入的了解,你的编程之路才能走的更顺更远!!!
希望我们一起进步!!
欢迎大家的点评!