前言
这篇文章将深入探讨单向链表,这是计算机科学和数据结构中的一个基本概念。如果你是一位程序员、计算机科学学生或对数据结构和算法感兴趣的任何人,阅读本文将为你提供以下优势和好处:
-
深入理解单向链表:我们将详细介绍单向链表的结构和操作,从基本概念到高级应用,让你全面了解它的工作原理。
-
常用接口的实现:本文将提供单向链表的常用操作接口的实现,包括插入、删除、查找、修改、反转、获取长度和清空等,让你掌握如何操作链表的技能。
-
代码示例:每个操作接口都将伴随着详细的代码示例,帮助你直观地理解如何编写链表操作的代码。
-
应用广泛:单向链表是许多其他数据结构和算法的基础,理解它对于编写高效的程序和解决复杂的计算问题至关重要。
注意: 如果你想简单地浏览文章,了解单向链表的基本概念和操作,可能只需要大约 15到20分钟 。 但如果你想深入学习,并逐步理解和实践文章中提到的代码示例,可能需要更多的时间,可能会花费1到2小时或更多 *。 当然,你也可以直接浏览本文章的第10部分,拿上代码直接用。
1. 简介
1.1 什么是单向链表?
单向链表是一种线性数据结构,由节点组成,每个节点包含数据和指向下一个节点的指针。
1.2 为什么使用单向链表?
使用单向链表可以动态分配内存,适应变化的数据,提高插入和删除效率。
2. 单向链表基本结构
2.1 结点定义
单向链表结点结构的定义大抵如下:
C
struct Node{
elementType1 element;
elementType2 element2;
...
struct Node *next;
}
本文采用如下的结点定义
C
typedef struct linkedListNode linkedList;
typedef linkedList Node;
typedef int elementType;
struct linkedListNode
{
elementType value;
linkedList *next;
};
2.2 创建新结点
- 在内存中申请一片空间,并命名为
node
。 - 我们让这片空间的元素域(本文中称为
value
)赋值为传入的value
的值,并奖指针域(next
)指向NULL
。 - 将这片空间的地址返回。
函数的实现如下:
C
Node *createNode(elementType value)
{
Node *node = (Node *)malloc(sizeof(Node));
node->value = value;
node->next = NULL;
return node;
}
2.3 头指针和尾指针
声明如下(这篇文章中只需要头指针)
C
struct Node *head=NULL;
//struct Node *tail=NULL;
回答3个问题:
-
什么是头指针和尾指针? 头指针是指向链表第一个节点的指针,尾指针是指向链表最后一个节点的指针。
-
头指针有何作用? 头指针用于访问链表的第一个节点,是链表的入口。
-
尾指针有何作用? 尾指针用于快速定位链表的最后一个节点,以便在尾部插入操作时提高效率。
3. 插入操作
3.1 无返回值的插入函数
由于无返回值,这意味着我们需要原地修改传入的链表。 那么问题来了:我们如何原地修改呢? 不妨类比一下swap函数:
C
//交换两个int类型
void swap(int *a,int *b)
{
int temp=*a;
*a=*b;
*b=temp;
}
或者
C
//交换两个int类型一维数组
void swap(int **a, int **b)
{
int *temp = *a;
*a = *b;
*b = temp;
}
答案显而易见:指针!
3.1.1 插入头部
函数接口如下: void insertAtHead(struct Node **head,elementType newValue);
- 创建一个新的节点,并存入将要插入的数据.
- 将新节点的指针域(本文中称为
next
)指向当前链表的头节点. - 更新链表的头指针,使其指向新节点,从而将新节点变成链表的新头部。
函数的实现如下:
C
void insertAtHead(linkedList **head, elementType newValue)
{
Node *newNode = createNode(newValue);
newNode->next = *head;
*head = newNode;
}
3.1.2 插入尾部
- 创建一个新的节点,将要插入的数据存储在这个节点中。
- 找到链表的尾节点,这通常需要遍历整个链表,直到找到最后一个节点。
- 将新节点的指针域(通常称为
next
或link
)指向NULL
,表示新节点将成为尾节点。 - 将尾节点的指针域指向新节点,以将新节点连接到链表的末尾。
下面是一个示例,演示如何在单向链表中插入尾部:
c
void insertAtTail(linkedList **head, elementType newValue) {
Node *newNode = createNode(newValue);
// 如果链表为空,新节点成为链表的头节点
if (*head == NULL) {
*head = newNode;
return;
}
// 否则,找到链表的尾节点
Node *tailNode = *head;
while (tailNode->next != NULL) {
tailNode = tailNode->next;
}
// 将新节点连接到尾节点
tailNode->next = newNode;
}
这个函数首先创建一个新节点,然后检查链表是否为空。如果链表为空,新节点将成为链表的头节点。如果链表不为空,它会遍历链表以找到尾节点,然后将新节点连接到尾节点,使其成为新的尾节点。
3.1.3 插入任意位置
- 创建一个新的节点,将要插入的数据存储在这个节点中。
- 找到插入位置的前一个节点(前驱节点),这通常需要遍历链表,直到找到前驱节点。
- 将新节点的指针域指向前驱节点原来指向的节点。
- 将前驱节点的指针域指向新节点,以将新节点插入到链表中。
函数实现如下:
c
void insertNodeAtLocation(linkedList **head, elementType value, int location) {
if (location < 1 || (!(*head) && location > 1)) {
printf("Error: 插入位置(%d)太小。\n", location);
return;
}
// 如果插入位置是1,等同于在头部插入
if (location == 1) {
insertAtHead(head, value);
return;
}
Node *prevNode = findNodeByLocation(*head, location - 1);
if (!prevNode) {
printf("Error: 插入位置(%d)太大。\n", location);
} else {
Node *node = createNode(value);
node->next = prevNode->next;
prevNode->next = node;
}
}
这个函数首先检查插入位置是否有效,如果位置小于1或者链表为空且位置大于1,会产生错误。如果插入位置是1,它会调用之前实现的插入头部的函数。否则,它会找到插入位置的前一个节点(前驱节点),然后插入新节点到前驱节点后面。
3.2 有返回值的插入函数
有返回值的插入函数允许插入新节点并返回插入后的链表。这些函数通常用于验证插入操作是否成功,以及在需要时检查链表状态。
3.2.1 插入头部
插入头部的有返回值函数接口如下:
c
Node *insertNodeAtHead(linkedList *head, elementType value);
这个函数首先创建一个新的节点,并将要插入的数据赋值给新节点的value
字段。然后,它将新节点的指针域next
指向当前链表的头节点,从而将新节点插入到链表的头部。最后,返回链表的新头节点。
函数的实现如下:
c
Node *insertNodeAtHead(linkedList *head, elementType value) {
Node *newNode = createNode(value);
newNode->next = head;
return newNode;
}
3.2.2 插入尾部
插入尾部的有返回值函数接口如下:
c
Node *insertNodeAtTail(linkedList *head, elementType value);
这个函数首先创建一个新的节点,并将要插入的数据赋值给新节点的value
字段。然后,它遍历链表,找到尾节点,将尾节点的指针域next
指向新节点,从而将新节点插入到链表的尾部。最后,返回链表的头节点。
函数的实现如下:
c
Node *insertNodeAtTail(linkedList *head, elementType value) {
Node *newNode = createNode(value);
if (!head) {
return newNode;
}
Node *tailNode = head;
while (tailNode->next) {
tailNode = tailNode->next;
}
tailNode->next = newNode;
return head;
}
3.2.3 插入任意位置
插入任意位置的有返回值函数接口如下:
c
Node *insertNodeAtLocation(linkedList *head, elementType value, int location);
这个函数首先检查插入位置是否有效,如果位置小于1或链表为空且位置大于1,则插入失败,返回原链表。如果插入位置为1,则调用插入头部的函数,将新节点插入到链表头部。否则,使用循环找到插入位置前一个节点,然后插入新节点到该位置。最后,返回链表的头节点。
函数的实现如下:
c
Node *insertNodeAtLocation(linkedList *head, elementType value, int location) {
if (location < 1 || (!head && location > 1)) {
printf("Error: Insertion location(%d) is invalid.\n", location);
return head;
}
if (location == 1) {
return insertNodeAtHead(head, value);
}
Node *prevNode = findNodeByLocation(head, location - 1);
if (!prevNode) {
printf("Error: Insertion location(%d) is too large.\n", location);
return head;
}
Node *node = createNode(value);
node->next = prevNode->next;
prevNode->next = node;
return head;
}
4. 删除操作
删除操作是单向链表中的重要操作之一,它允许从链表中移除节点。根据需求,你可以实现不同类型的删除操作,包括删除头节点、删除尾节点和删除任意位置的节点。
4.1 无返回值的删除函数
4.1.1 删除头节点
删除头节点是将链表的第一个节点移除的操作。这通常包括以下步骤:
- 将链表的头指针指向第一个节点的下一个节点。
- 释放被删除节点的内存空间。
下面是一个示例,演示如何删除单向链表的头节点:
c
void deleteNodeAtHead(linkedList **head) {
if (*head == NULL) {
printf("Error: 链表为空,无法删除头节点。\n");
return;
}
Node *thisNode = *head;
*head = (*head)->next;
free(thisNode);
}
这个函数首先检查链表是否为空。如果链表为空,则无法删除头节点。否则,它将头指针指向第一个节点的下一个节点,并释放被删除节点的内存空间。
4.1.2 删除尾节点
删除尾节点是将链表的最后一个节点移除的操作。这通常需要找到倒数第二个节点,然后将其指针域指向NULL
,从而将尾节点移除。下面是一个示例:
c
void deleteNodeAtTail(linkedList **head) {
if (*head == NULL || (*head)->next == NULL) {
printf("Error: 链表为空或只有一个节点,无法删除尾节点。\n");
return;
}
Node *prevNode = *head;
Node *tailNode = (*head)->next;
while (tailNode->next != NULL) {
prevNode = tailNode;
tailNode = tailNode->next;
}
prevNode->next = NULL;
free(tailNode);
}
这个函数首先检查链表是否为空或只有一个节点。如果是这种情况,无法删除尾节点。否则,它会找到倒数第二个节点(前驱节点),然后将其指针域指向NULL
,并释放被删除节点的内存空间。
4.1.3 删除任意位置的节点
删除任意位置的节点需要指定要删除的位置,并在找到位置后执行删除操作。下面是一个示例:
c
void deleteNodeAtLocation(linkedList **head, int location) {
if (location < 1 || !(*head)) {
printf("Error: 删除位置(%d)太小或链表为空。\n", location);
return;
}
if (location == 1) {
deleteNodeAtHead(head);
return;
}
Node *prevNode = findNodeByLocation(*head, location - 1);
if (!prevNode) {
printf("Error: 删除位置(%d)太大。\n", location);
} else {
Node *deleteNode = prevNode->next;
prevNode->next = deleteNode->next;
free(deleteNode);
}
}
这个函数首先检查删除位置是否有效以及链表是否为空。如果位置小于1或链表为空,无法进行删除操作。如果删除位置是1,它会调用之前实现的删除头节点的函数。否则,它会找到删除位置的前一个节点(前驱节点),然后将前驱节点的指针域指向要删除节点的下一个节点,并释放被删除节点的内存空间。
4.2 有返回值的删除函数
有返回值的删除函数允许删除节点并返回删除后的链表。这些函数通常用于验证删除操作是否成功,以及在需要时检查链表状态。
4.2.1 删除头结点
删除头结点的有返回值函数接口如下:
c
Node *deleteNodeAtHead(linkedList *head);
这个函数首先检查链表是否为空,如果为空则返回NULL
。如果链表不为空,它会将头节点保存到一个临时变量中,然后将头节点的下一个节点作为新的头节点,并释放掉原头节点的内存。最后,返回链表的新头节点。
函数的实现如下:
c
Node *deleteNodeAtHead(linkedList *head) {
if (!head) {
return NULL;
}
Node *thisNode = head;
head = head->next;
free(thisNode);
return head;
}
4.2.2 删除尾结点
删除尾结点的有返回值函数接口如下:
c
Node *deleteNodeAtTail(linkedList *head);
这个函数首先检查链表是否为空或是否只有一个节点,如果是这两种情况则返回NULL
。如果链表有多个节点,它会遍历链表找到尾节点的前一个节点,然后将前一个节点的指针域next
置为NULL
,释放掉尾节点的内存。最后,返回链表的头节点。
函数的实现如下:
c
Node *deleteNodeAtTail(linkedList *head) {
if (!head || !head->next) {
return NULL;
}
Node *prevNode = head;
Node *tailNode = head->next;
while (tailNode->next) {
prevNode = tailNode;
tailNode = tailNode->next;
}
prevNode->next = NULL;
free(tailNode);
return head;
}
4.2.3 删除任意位置的结点
删除任意位置的结点的有返回值函数接口如下:
c
Node *deleteNodeAtLocation(linkedList *head, int location);
这个函数首先检查删除位置是否有效,如果位置小于1或链表为空,则删除失败,返回原链表。如果删除位置为1,则调用删除头结点的函数,删除链表的头节点。否则,使用循环找到删除位置前一个节点,然后将前一个节点的指针域next
指向删除位置后一个节点,释放掉被删除节点的内存。最后,返回链表的头节点。
函数的实现如下:
c
Node *deleteNodeAtLocation(linkedList *head, int location) {
if (location < 1 || !head) {
printf("Error: Deletion location(%d) is invalid or the list is empty.\n", location);
return head;
}
if (location == 1) {
return deleteNodeAtHead(head);
}
Node *prevNode = findNodeByLocation(head, location - 1);
if (!prevNode) {
printf("Error: Deletion location(%d) is too large.\n", location);
return head;
}
Node *deleteNode = prevNode->next;
prevNode->next = deleteNode->next;
free(deleteNode);
return head;
}
5. 查找操作
查找操作允许你在单向链表中查找特定值或位置的节点。这对于检索数据非常有用。
5.1 根据节点值查找节点
根据节点值查找节点是通过遍历链表来寻找包含特定值的节点。下面是一个示例:
c
Node *findNodeByValue(linkedList *head, elementType value) {
while (head) {
if (head->value == value) {
return head;
}
head = head->next;
}
return NULL;
}
这个函数遍历链表,逐个比较节点的值与目标值,如果找到匹配的节点,则返回该节点的指针。如果遍历整个链表仍然没有找到匹配的节点,则返回NULL
表示未找到。
5.2 根据节点位置查找节点
根据节点位置查找节点是通过指定位置来查找链表中的节点。下面是一个示例:
c
Node *findNodeByLocation(linkedList *head, int location) {
if (location < 1) {
printf("Error: 查找位置(%d)太小。\n", location);
return NULL;
}
for (int i = 0; i < location - 1; i++) {
head = head->next;
if (!head) {
printf("Error: 查找位置(%d)太大。\n", location);
return NULL;
}
}
return head;
}
这个函数首先检查查找位置是否有效(大于等于1)。然后,它遍历链表,直到找到指定位置的节点。如果找到位置太大而超出了链表长度,它会返回NULL
表示未找到。
5.3 检查链表中是否存在特定的值
检查链表中是否存在特定值的操作非常简单,可以利用前面实现的根据节点值查找节点的函数。如果根据值找到了节点,则说明该值存在于链表中;否则,该值不存在。
c
bool containsValueInLinkedList(linkedList *head, elementType value) {
return findNodeByValue(head, value) != NULL;
}
这个函数使用之前的查找函数,如果找到了节点,则返回true
表示存在,否则返回false
表示不存在。
6. 修改操作
修改操作允许你更新链表中特定节点的值。这对于更新数据非常有用。
c
void modifyNodeValue(linkedList **head, int location, elementType newValue) {
Node *node = findNodeByLocation(*head, location);
if (node) {
node->value = newValue;
}
}
这个函数首先使用之前实现的根据位置查找节点的函数找到要修改的节点,然后将其值更新为新值。
7. 反转操作
反转操作是将链表中的节点顺序颠倒,使原先的尾节点成为新的头节点。这可以通过递归或迭代方式实现。
7.1 递归实现反转
递归实现反转需要考虑两种情况:链表为空或链表只有一个节点时,无需反转;链表有多个节点时,需要进行反转。
c
Node *reverseLinkedList(linkedList *head) {
// 递归实现
if (!head || !head->next) {
return head;
}
Node *node = reverseLinkedList(head->next);
head->next->next = head;
head->next = NULL;
return node;
}
这个递归函数首先检查链表是否为空或只有一个节点。如果是这种情况,无需反转,直接返回链表头节点。如果链表有多个节点,它会递归调用自身来反转子链表,然后将当前节点的下一个节点的指针域指向当前节点,最后将当前节点的指针域置为NULL
,完成反转操作。
7.2 迭代实现反转
迭代实现反转需要使用三个指针:当前节点、前一个节点和下一个节点。通过不断更新这些指针的位置,可以实现链表的反转。
c
Node *reverseLinkedListIterative(linkedList *head) {
Node *prev = NULL;
Node *current = head;
Node *next = NULL;
while (current != NULL) {
next = current->next;
current->next = prev;
prev = current;
current = next;
}
return prev;
}
这个迭代函数从链表的头节点开始,依次遍历每个节点。在每一步中,它将当前节点的指针域指向前一个节点,然后更新三个指针的位置,最终完成链表的反转。
8. 获取链表长度
获取链表长度是一个简单的操作,只需遍历链表并计数节点数量即可。
c
int getLengthOfLinkedList(linkedList *head) {
int length = 0;
while (head) {
head = head->next;
length++;
}
return length;
}
这个函数初始化一个计数器,然后遍历链表并逐个增加计数器的值,直到遍历完整个链表,返回计数器的值作为链表的长度。
9. 清空链表
清空链表是将链表中所有节点释放掉,使链表变为空链表的操作。
c
void clearLinkedList(linkedList **head) {
if (!(*head)) {
return;
}
clearLinkedList(&(*head)->next);
free(*head);
*head = NULL;
}
这个函数使用递归方式遍历链表,并释放每个节点的内存空间。最后,将链表的头指针置为NULL
,使链表变为空链表。
以上是单向链表常用接口的具体实现和操作,这
些操作允许你灵活地对单向链表进行插入、删除、查找、修改、反转、获取长度等操作。通过了解和使用这些接口,你可以更好地利用单向链表来处理各种数据结构问题。希望这些信息对你有所帮助!
10. 分享一下我自己用C语言写的链表(代码自取)(全)
分享一下我自己写的链表的接口:
C
// my_single_basic_linklist.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct linkedListNode linkedList;
typedef linkedList Node;
typedef int elementType;
struct linkedListNode
{
elementType value;
linkedList *next;
};
// 创建一个新节点
Node *createNode(elementType value);
// 根据节点值查找链表中的节点
Node *findNodeByValue(linkedList *head, elementType value);
// 根据节点位置查找链表中的节点
Node *findNodeByLocation(linkedList *head, int location);
// 检查链表中是否存在特定的值
bool containsValueInLinkedList(linkedList *head, elementType value);
// 获取链表的长度
int getLengthOfLinkedList(linkedList *head);
// 在链表的指定位置插入节点
void insertNodeAtLocation(linkedList **head, elementType value, int location);
// 在链表头部插入节点
void insertNodeAtHead(linkedList **head, elementType value);
// 在链表尾部插入节点
void insertNodeAtTail(linkedList **head, elementType value);
// 从链表的指定位置删除节点
void deleteNodeAtLocation(linkedList **head, int location);
// 删除链表头部的节点
void deleteNodeAtHead(linkedList **head);
// 删除链表尾部的节点
void deleteNodeAtTail(linkedList **head);
// 修改链表中某个节点的值
void modifyNodeValue(linkedList **head, int location, elementType newValue);
// 遍历链表并执行操作
void traverseLinkedList(linkedList *head, void (*operation)(elementType));
// 反转链表,递归实现
Node *reverseLinkedList(linkedList *head);
// 清空链表并释放内存
void clearLinkedList(linkedList **head);
函数的实现如下:
C
// my_single_basic_linklist.c
#include "my_single_basic_linklist.h"
// 创建一个新节点
Node *createNode(elementType value)
{
Node *node = (Node *)malloc(sizeof(Node));
node->value = value;
node->next = NULL;
return node;
}
// 根据节点值查找链表中的节点
Node *findNodeByValue(linkedList *head, elementType value)
{
while (head)
{
if (head->value == value)
{
return head;
}
head = head->next;
}
return NULL;
}
// 根据节点位置查找链表中的节点
Node *findNodeByLocation(linkedList *head, int location)
{
if (location < 1)
{
printf("Error:findNodeByLocation:loacation(%d)<1\n", location);
return NULL;
}
for (int i = 0; i < location - 1; i++)
{
head = head->next;
if (!head)
{
printf("Error:findNodeByLocation:location(%d) too big.\n", location);
return NULL;
}
}
return head;
}
// 检查链表中是否存在特定的值
bool containsValueInLinkedList(linkedList *head, elementType value)
{
return findNodeByValue(head, value) != NULL;
}
// 获取链表的长度
int getLengthOfLinkedList(linkedList *head)
{
int length = 0;
while (head)
{
head = head->next;
length++;
}
return length;
}
// 在链表的指定位置插入节点
void insertNodeAtLocation(linkedList **head, elementType value, int location)
{
if (location < 1 || (!(*head) && location > 1))
{
printf("Error:insertNodeAtLocation:location(%d) too small.\n", location);
return;
}
if (location == 1)
{
insertNodeAtHead(head, value);
return;
}
Node *prevNode = findNodeByLocation(*head, location - 1);
if (!prevNode)
{
printf("Error:insertNodeAtLocation:location(%d) too big.\n", location);
}
else
{
Node *node = createNode(value);
node->next = prevNode->next;
prevNode->next = node;
}
}
// 在链表头部插入节点
void insertNodeAtHead(linkedList **head, elementType value)
{
Node *node = createNode(value);
node->next = *head;
*head = node;
}
// 在链表尾部插入节点
void insertNodeAtTail(linkedList **head, elementType value)
{
Node *node = createNode(value);
Node *tailNode = *head;
while (tailNode->next)
{
tailNode = tailNode->next;
}
tailNode->next = node;
tailNode = node;
}
// 从链表的指定位置删除节点
void deleteNodeAtLocation(linkedList **head, int location)
{
if (location < 1 || !(*head))
{
printf("Error:deleteNodeAtLocation:location(%d) too small or this head is null.\n", location);
return;
}
Node *prevNode = findNodeByLocation(*head, location - 1);
if (!prevNode)
{
printf("Error:deleteNodeAtLocation:location(%d) too big.\n", location);
}
else
{
Node *deleteNode = prevNode->next;
prevNode->next = deleteNode->next;
free(deleteNode);
}
}
// 删除链表头部的节点
void deleteNodeAtHead(linkedList **head)
{
Node *thisNode = *head;
*head = (*head)->next;
free(thisNode);
}
// 删除链表尾部的节点
void deleteNodeAtTail(linkedList **head)
{
// Node *tailNode = *head;
// while (tailNode->next)
// {
// tailNode = tailNode->next;
// }
// free(tailNode);
// tailNode = NULL;
if (!(*head) || !(*head)->next)
{
printf("Error:deleteNodeAtTail: List is empty or has only one node.\n");
return;
}
Node *prevNode = *head;
Node *tailNode = (*head)->next;
while (tailNode->next)
{
prevNode = tailNode;
tailNode = tailNode->next;
}
prevNode->next = NULL;
free(tailNode);
}
// 修改链表中某个节点的值
void modifyNodeValue(linkedList **head, int location, elementType newValue)
{
Node *node = findNodeByLocation(*head, location);
if (node)
{
node->value = newValue;
}
}
Node *reverseLinkedList(linkedList *head)
{
// 递归实现
if (!head || !head->next)
{
return head;
}
Node *node = reverseLinkedList(head->next);
head->next->next = head;
head->next = NULL;
return node;
}
// 遍历链表并执行操作
void traverseLinkedList(linkedList *head, void (*operation)(elementType))
{
while (head)
{
operation(head->value);
head = head->next;
}
}
// 清空链表并释放内存
void clearLinkedList(linkedList **head)
{
if (!(*head))
{
return;
}
clearLinkedList(&(*head)->next);
free(*head);
*head = NULL;
}
11. 总结
在这篇文章中,我们详细介绍了单向链表的基本结构和常用操作接口的实现。我们了解了链表节点的定义,创建新节点的方法,以及如何使用头指针和尾指针来管理链表。接着,我们介绍了插入、删除、查找、修改、反转、获取长度和清空等操作的具体实现方法。通过这些操作,你可以轻松地操作和管理单向链表,应用于各种数据结构和算法问题中。
希望这篇文章能够帮助你更深入地理解和应用单向链表,为你的编程和算法学习提供帮助。