1. 链表概念
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。由一系列节点(Node)通过指针连接而成(像火车),从一个头节点(Head)开始,头节点作为链表的入口点,它包含了对第一个节点的引用。最后一个节点的指针指向一个空值(NULL),表示链表的结束。一个节点有数据域和指针域(节点相当于数组的元素)
【存储】链表在内存中的存储方式则是随机存储(见缝插针),每一个节点分布在内存的不同位置,依靠指针关联起来。(只要有足够的内存空间,就能为链表分配内存)
【优缺点】:相对于顺序储存(例如数组):
优点:链表的插入操作更快( O(1) ),无需预先分配内存空间
缺点:失去了随机读取的优点(需要从头节点开始依次遍历,直到找到目标节点。),内存消耗较大(每个节点都需要存储指向下一个节点的指针)。
2. 链表分类
【1】带头结点和不带头结点:头结点是第一个节点的数据域不使用,只使用next域
【2】头插法和尾插法
头插法:在头指针的后面插入新节点,相当于后进先出(栈)。因为链表是从头部开始遍历
尾插法::在尾部插入新节点,相当于后进后出(队列)。
【3】循环链表,单向链表,双向链表
虽然链表中存在这么多结构,但是在实际中最常用的就只有两种结构:
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单。
3. 单链表的实现
包含节点的创建,插入,删除,修改,查找,遍历打印
相关代码的示意图
【1】节点的插入
【2】删除节点
有人可能会不理解,为什么要搞两个指针来完成删除,或者其他操作,因为我们要用其他指针来保存节点的信息,需要两个指针来分别跟踪当前节点和前一个节点,这样才能正确地修改链表结构。我的理解是,把这个指针当做是这个结点,总不能对一个不知道名字的节点进行操作吧。
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构
typedef struct node_s
{
int data;
struct node_s* next;
}node_t;
//定义头指针和尾指针
typedef struct linkedlist_s
{
node_t *head;
node_t *tail;
}linkedlist_t;
void link_init(linkedlist_t *list);
node_t* link_create_node(int data);
void link_insert_head(linkedlist_t *link, int data);
void link_insert_tail(linkedlist_t *list, int data);
void link_delete_node(linkedlist_t *list, int value);
void link_update_node(linkedlist_t *list, int oldvalue, int newvalue);
node_t* link_search_node(linkedlist_t *list, int value);
void link_print(linkedlist_t *list);
int main (int argc, char **argv)
{
linkedlist_t list;
link_init(&list);
link_insert_head(&list, 10);
link_insert_tail(&list, 20);
link_insert_tail(&list, 30);
link_insert_head(&list, 5);
printf("链表内容: ");
link_print(&list);
link_update_node(&list, 20, 25);
printf("更新后的链表内容: ");
link_print(&list);
link_delete_node(&list, 10);
printf("删除后的链表内容: ");
link_print(&list);
node_t* node = link_search_node(&list, 25);
if (node != NULL) {
printf("找到节点: %d\n", node->data);
} else {
printf("未找到节点\n");
}
return 0;
}
//初始化头,尾指针
void link_init(linkedlist_t *list)
{
list->head = NULL;
list->tail = NULL;
}
//创建一个节点
node_t* link_create_node(int data)
{
node_t* new_node= malloc( sizeof(node_t) );
if( new_node == NULL)
{
printf("分配失败\n");
exit(EXIT_FAILURE);
}
new_node->data = data;
new_node->next = NULL;
return new_node;
}
//头插法
void link_insert_head(linkedlist_t *list, int data)
{
node_t *new_node = link_create_node(data);
if( list->head == NULL)
{
list->head = new_node;
list->tail = new_node;
}
new_node->next = list->head;//指向之前的头结点
list->head = new_node;//头指针指向新插入的结点
}
//尾插法
void link_insert_tail(linkedlist_t *list, int data)
{
node_t *new_node = link_create_node(data);
if( list->head == NULL)
{
list->head = new_node;
list->tail = new_node;
}
list->tail->next = new_node;
list->tail = new_node;
}
//删除某个特定节点
void link_delete_node(linkedlist_t *list, int value)
{
if(list->head == NULL)
return;
node_t *temp = list->head;
if(temp->data = value)//如果是头节点
{
list->head = temp->next;
if(list->head == NULL)
{
list->tail = NULL;//说明只有一个节点,删完就没有了
}
free(temp);
return;
}
node_t *prev = NULL;
while(temp != NULL && temp->data != value)
{
prev = temp;//没有找到继续往后找
temp = temp->next;
}
if( temp == NULL)
return;
prev->next = temp->next;//找到后,前一个节点指向当前节点的后一个节点
if( prev->next == NULL)
list->tail = prev;
free(temp);
}
//修改某个节点的值
void link_update_node(linkedlist_t *list, int oldvalue, int newvalue)
{
node_t *temp = list->head;
while( temp != NULL )
{
if( temp->data = oldvalue)
{
temp->data = newvalue;
return;
}
temp = temp->next;//否则一直找
}
printf("节点%d未找到\n",oldvalue);
}
//按值查找节点
node_t* link_search_node(linkedlist_t *list, int value)
{
node_t *temp = list->head;
while( temp !=NULL )
{
if( temp->data == value )
{
return temp;
}
temp = temp->next;
}
printf("没有找到相关结点\n");
return NULL;
}
//遍历链表的每个节点并打印
void link_print(linkedlist_t *list)
{
node_t *temp = list->head;
while( temp != NULL )
{
printf("%d->",temp->data);
temp = temp->next;
}
printf("NULL\n");//打印完毕
}
4. 双向链表
【1】为什么使用双向链表
单链表的结点中只有一个指向其后继的指针,使得单链表要访问某个结点的前驱结点时,只能从头开始遍历,访问后驱结点的复杂度为O(1),访问前驱结点的复杂度为O(n)。(例如,若实际问题中需要频繁地查找某个结点的前驱结点,使用单链表存储数据显然没有优势,因为单链表的强项是从前往后查找目标元素,不擅长从后往前查找元素。)为了克服上述缺点,引入了双链表。
【2】定义
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。
双向链表中各节点包含以下 3 部分信息:
- 指针域:用于指向当前节点的直接前驱节点;
- 数据域:用于存储数据元素。
- 指针域:用于指向当前节点的直接后继节点;
【3】特点
1.每次在插入或删除某个节点时, 需要处理四个节点的引用, 而不是两个,实现起来要困难一些;
2.相对于单向链表, 必然占用内存空间更大一些;
3.既可以从头遍历到尾, 又可以从尾遍历到头;
【4】结构体
// 定义双向链表节点结构
typedef struct line{
struct line * prior; //指向直接前趋
int data;
struct line * next; //指向直接后继
}Line;
// 定义双向链表结构
typedef struct {
Node* head;
Node* tail;
} DoublyLinkedList;
参考链接: 数据结构之链表---从入门到精通