书接上回,线性表的顺序表示已经结束,简单的再对线性表的链式实现解释一下。
一.概述
线性表的顺序存储结构的特点是逻辑关系上相邻的两个元素在物理位置上也相邻,因此可以随机存取表中的任一元素,它的存储位置可以用一个简单、直观的公式来表示。
这个特点也铸成了这种存储结构的缺点:在插入、删除操作时,需要移动大量的元素。而线性表的另一种表示方法——链存储结构刚好弥补了它的缺点。
链存储结构不要求逻辑上相邻的元素在物理位置上也相邻,因此它没有顺序存储结构所有具有的缺点,但同时也失去了顺序存储结构可随机存取的优点。
链存储结构的特点是元素可以使用存储内存中的任何位置(可以是连续的,也可以不连续),元素a[i]和a[i+1]的逻辑关系不依靠相对位置,而是元素中增加一个指示其后继元素的数据(元素指针),元素本身的数据+后继信息构成了存储映像,俗称节点(node)。
链式存储的元素结构
typedef struct Node
{
TYPE data; // 数据域
struct Node* next; // 指针域
}Node;
二.线性表的链式表示和实现
若干个元素节点通过指针域连接起来,形成的线性表结构称为链式表,简称链表,节点中只有一个指向后继元素的指针域,这种链表也被称为单向链表。
单向链表必须有一个指向第一个节点的指针,被称为头指针,被它指向的节点也被称为头节点,头节点可以不存储数据,单纯的作为一个占位节点,最后一个节点指向空,作为结束标志。
我们以最简单的单向链表为例
1.不带头结点的单向链表
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define TYPE int
typedef struct Node
{
TYPE data;
struct Node* next;
}Node;
// 创建节点
Node* create_node(TYPE data)
{
// 创建节点内存
Node* node = malloc(sizeof(Node));
// 赋值数据域
node->data = data;
// 初始化指针域
node->next = NULL;
return node;
}
// 头添加元素
void front_list(Node** head,TYPE data)
{
// 创建节点
Node* node = create_node(data);
node->next = *head;
*head = node;
}
// 删除元素
bool delete_index_list(Node** head,int index)
{
// 删除每个节点,因为第一个节点没有前驱
if(0 == index)
{
Node* node = *head;
*head = (*head)->next;
free(node);
return true;
}
// 找到要删除的节点的前驱
Node* prev = *head;
while(NULL!=prev->next && index-->1)
prev = prev->next;
if(NULL != prev->next)
{
// 备份要删除的节点
Node* node = prev->next;
// 前驱节点的指针域指向后继节点
prev->next = prev->next->next;
free(node);
return true;
}
return false;
}
// 插入元素
bool insert_list(Node** head,int index,TYPE data)
{
Node* node = create_node(data);
if(0 == index)
{
node->next = *head;
*head = node;
return true;
}
Node* prev = *head;
while(NULL!=prev->next && index-->1)
prev = prev->next;
if(NULL != prev->next)
{
node->next = prev->next;
prev->next = node;
return true;
}
return false;
}
// 遍历链表
void show_list(Node* head)
{
for(Node* n=head; NULL!=n; n=n->next)
{
printf("%d ",n->data);
}
printf("\n");
}
2.带头结点的单向链表
在执行链表的插入、删除操作时,需要被操作节点的前驱节点和后继节点,如果被操作节点是头节点,则它没有前驱节点,需要额外特殊处理,因此为了方便插入和删除操作所以给单链表增加一个空的头节点。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define TYPE int
typedef struct Node
{
TYPE data;
struct Node* next;
}Node;
// 创建节点
Node* create_node(TYPE data)
{
Node* node = malloc(sizeof(Node));
node->data = data;
node->next = NULL;
return node;
}
static bool _insert_list(Node* prev,TYPE data)
{
if(NULL == prev)
return false;
Node* node = create_node(data);
node->next = prev->next;
prev->next = node;
return true;
}
// 根据前驱节点删除节点
static bool _delete_list(Node* prev)
{
if(NULL == prev || NULL == prev->next)
return false;
Node* node = prev->next;
prev->next = node->next;
free(node);
return true;
}
// 返回index下标的前驱节点
static Node* _index_list(Node* head,int index)
{
// 找下标为i的节点的前驱
Node* prev = head;
while(NULL != prev->next && index-- >= 1)
prev = prev->next;
// index 非法,超出了节点的数量
if(NULL == prev->next || index < -1)
return NULL;
return prev;
}
// 查询出值为data的前驱节点
static Node* _query_list(Node* head,TYPE data)
{
Node* prev = head;
while(NULL != prev->next && data != prev->next->data)
prev = prev->next;
if(NULL == prev->next)
return NULL;
return prev;
}
// 创建带头的空链表
Node* create_list(void)
{
Node* node = malloc(sizeof(Node));
node->next = NULL;
return node;
}
// 销毁链表
void destory_list(Node* head)
{
while(NULL != head)
{
Node* node = head;
head = head->next;
free(node);
}
}
// 在头部添加元素
void front_list(Node* head,TYPE data)
{
_insert_list(head,data);
}
// 在指定位置插入元素
bool insert_list(Node* head,int index,TYPE data)
{
return _insert_list(_index_list(head,index),data);
}
// 删除指定位置的元素
bool delete_index_list(Node* head,int index)
{
return _delete_list(_index_list(head,index));
}
// 按值删除元素
bool delete_value_list(Node* head,TYPE data)
{
return _delete_list(_query_list(head,data));
}
// 按值修改
bool modify_value_list(Node* head,TYPE old,TYPE new)
{
Node* prev = _query_list(head,old);
if(NULL == prev)
return false;
prev->next->data = new;
return true;
}
// 按位置修改
bool modify_index_list(Node* head,int index,TYPE new)
{
Node* prev = _index_list(head,index);
if(NULL == prev)
return false;
prev->next->data = new;
return true;
}
// 访问指定位置的元素
bool get_list(Node* head,int index,TYPE* data)
{
Node* prev = _index_list(head,index);
if(NULL == prev)
return false;
*data = prev->next->data;
return true;
}
// 查询元素
int query_list(Node* head,TYPE key)
{
int index = 0;
for(Node* n=head->next; NULL!=n; n=n->next)
{
if(n->data == key)
return index;
index++;
}
return -1;
}
// 经典排序
void sort_list(Node* head)
{
for(Node* i=head->next; NULL!=i->next; i=i->next)
{
for(Node* j=i->next; NULL!=j; j=j->next)
{
if(i->data > j->data)
{
TYPE tmp = i->data;
i->data = j->data;
j->data = tmp;
}
}
}
}
//遍历链表
void show_list(Node* head)
{
// 要跳过头节点
for(Node* n=head->next; NULL!=n; n=n->next)
{
printf("%d ",n->data);
}
printf("\n");
}
三.链式存储和顺序存储的优缺点
顺序存储的优点
1.可以随机访问:顺序存储其实就是数组,它的访问速度很快,可以实现对元素的随机访问,时间复杂度为O(1)
2.内存利用率高:元素存储的地址都是连续的
顺序存储的缺点
1.插入,删除效率低:越靠近头部的元素,插入,删除移动的元素越多
2.所需空间不准确:因为需要事先定义空间大小,空间太小溢出,空间太大浪费空间,但是柔性数组可以解决这个问题。
链表的优点
1.插入,删除操作方便:只需要改变相邻结点的指向即可插入,删除,效率大大增加
2.空间无需顾虑,因为空间是动态分配的,不会出现空间不足或者空间浪费
链表的缺点
1.不能随机访问元素:必须要从头开始查找,效率很低。最好的情况为O(1),最坏的情况为O(n).
2.空间利用率低:因为存储单元的碎片化,所以空间不会完全利用。
综上所述,当你需要大量的查找,访问元素时,采用顺序表来存储数据更加符合,而需要大量的插入数据删除数据时,则采用链表存储数据更好。
当然,这只是最简单的单向链表,还有双向链表,循环链表等等,以后再和大家分享。