了解完顺序表我们需要了解一下顺序表有哪些优缺点。
1.顺序表在头部和中间位置的插入删除都是O(N)
2.存在一定程度上的空间浪费。(比如1000个空间增容后只是使用了9个那么就浪费了991个空间)
3.顺序表增容的时候可能会发生将数据拷贝到另一个新开辟的空间然后释放旧空间,这是比较大的性能消耗。
根据顺序表的这些缺点,我们就需要学习新的数据结构来解决这些问题。由此就出来了链表。
链表的结构多样,总共有三种类型的属性:1.单行和双向 2.循环和不循环 3.带头和不带头
今天的第一个链表就是单向不带头不循环链表。
单向不带头不循环链表
单链表的结构
这个图展示的就是单链表的逻辑结构,第一个pList就是一个头节点的指针。每个节点都是由一个数据域和一个指针域组成的,指针域里面保存的就是下一个节点的地址,这样子就将所有的节点像是链子一样串在一起,这就是单链表。
各种链表的逻辑结构
1、单向链表和双向链表的逻辑结构
他们的区别就是单链表只有一个指针域指向下一个节点,双向链表有两个指针域一个指向下一个节点另一个指向前一个节点。
2、带头链表和不带头链表
带头的链表就是带一个哨兵位的头节点,这个头节点数据域不保存数据只有指针域指向第一个节点。这样可以更方便的找到链表的第一个节点。
3、循环和不循环链表
不循环链表的最后一个节点的指针域指向NULL,循环链表的指针域指向的是第一个节点,头尾相接就形成了一个循环链表。
根据上面这些小结构就可组成六种链表结构,其中简单的就是单项不带头不循环链表,简称单链表,STL中叫做forword_list,这种结构简单,但是一般不会直接使用,经常作为图的邻接表,邻接矩阵或者是哈希桶等高阶数据结构的子结构。
双向带头循环链表的结构最复杂但是实现起来确实最简单的。一般直接使用链表都是使用这个链表结构。
单链表的实现代码
//slist.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SListNode;
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
// 为什么不在pos位置之前插入?
//因为如果在pos位置之前插入一个值需要找到pos的前一个,就需要遍历链表时间复杂度是O(N)
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
// 为什么不删除pos位置?
// 删除pos位置需要将前后链接起来,找前就需要遍历链表时间复杂度为O(N)
void SListEraseAfter(SListNode* pos);
//在pos位置之前插入x
void SListInsert(SListNode** pplist,SListNode* pos, SLTDateType x);
//删除pos位置的值
void SListErase(SListNode** pplist ,SListNode* pos);
// 单链表的销毁
void SListDestroy(SListNode* plist);
这里的插入删除为什么传的是头节点指针的地址呢?因为这些接口里面可能会改变头节点,所以头节点变了头节点的指针也需要跟着变,改变一级指针就需要传一级指针的地址。如果是改变节点的成员,比如next之指针的指向,只需要一级指针就可以了,需要改变什么就要传什么的地址。
//slist.c
#define _CRT_SECURE_NO_WARNINGS
#include"slist.h"
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{
SListNode* node = (SListNode*)malloc(sizeof(SListNode));
node->data = x;
node->next = NULL;
return node;
}
// 单链表打印
void SListPrint(SListNode* plist)
{
SListNode* cur = plist;
while (cur)
{
printf("%d ", cur->data);
cur = cur->next;
}
puts("");
}
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
assert(pplist);
if (*pplist == NULL)
{
*pplist = BuySListNode(x);
}
else
{
SListNode* cur = *pplist;
while (cur->next)
{
cur = cur->next;
}
cur->next = BuySListNode(x);
}
}
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
assert(pplist);
if (*pplist == NULL)
{
*pplist = BuySListNode(x);
}
else
{
SListNode* newnode = BuySListNode(x);
newnode->next = *pplist;
*pplist = newnode;
}
}
// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
SListNode* cur = *pplist;
SListNode* prev = NULL;
while (cur->next)
{
prev = cur;
cur = cur->next;
}
prev->next = cur->next;
free(cur);
cur = NULL;
}
}
// 单链表头删
void SListPopFront(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
SListNode* cur = *pplist;
*pplist = cur->next;
free(cur);
cur = NULL;
}
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
assert(plist);
SListNode* node = plist;
while (node)
{
if (node->data == x)
return node;
node = node->next;
}
return NULL;
}
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
assert(pos);
SListNode* next = pos->next;
SListNode* newnode = BuySListNode(x);
pos->next = newnode;
newnode->next = next;
}
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{
assert(pos);
SListNode* next = pos->next;
pos->next = next->next;
free(next);
next = NULL;
}
//在pos位置之前插入x
void SListInsert(SListNode** pplist, SListNode* pos, SLTDateType x)
{
assert(pplist && pos);
if (*pplist == pos)
{
SListPushFront(pplist, x);
}
else
{
SListNode* cur = *pplist;
SListNode* prev = NULL;
while (cur)
{
if (cur == pos)
break;
prev = cur;
cur = cur->next;
}
SListNode* newnode = BuySListNode(x);
prev->next = newnode;
newnode->next = cur;
}
}
//删除pos位置的值
void SListErase(SListNode** pplist, SListNode* pos)
{
assert(pplist && pos);
if (*pplist == pos)
{
SListPopFront(pplist);
}
else
{
SListNode* prev = *pplist;
while (prev)
{
if (prev->next == pos)
break;
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
// 单链表的销毁
void SListDestroy(SListNode* plist)
{
assert(plist);
while (plist)
{
SListNode* next = plist->next;
free(plist);
plist = next;
}
}
数组模拟单链表
上面的单链表是用结构体来实现的,但是一般在算法题中我们都是使用的数组模拟单链表,比如图的邻接表,邻接矩阵,或者是哈希桶等等,使用数组模拟主要是可以避免malloc开辟空间的性能消耗,并且实现的代码更加简单。
数组模拟的原理就是使用两个数组,一个数组用来保存值val,另一个数组用来保存下一个节点的下标也就是next。然后一个head头指针,一个idx记录下标增长,避免使用到重复的数组空间。
下面来看一下代码的实现:
//N代表数组的大小也就是链表如果不删除的话的极限长度
const int N = 100010;
//head就是头节点的下标,e数组存放链表值val
//ne数组存放下一个节点的下标,idx保存的是数组增长到那个位置
int head,e[N],ne[N],idx;
void init()
{
head = -1;//初始head为-1,代表为NULL
idx = 0;
}
void add_to_head(int x)
{
e[idx] = x;//val==x
ne[idx] = head;//next = head;
head = idx;//head = idx;
idx++;
}
void erase_after(int k)
{
ne[k] = ne[ne[k]];//n = n->next->next;
//需要注意释放节点不需要释放内存空间,只需要跳过使用了的节点并且使用过的空间不可以重复使用
}
void insert_after(int k,int x)
{
e[idx] = x;//val==x
ne[idx] = ne[k];//n = k->next;
ne[k] = idx;//k->next = idx;
idx++;
}
数组比起来结构体代码更加简单,只有几个简单的接口,并且不需要释放和申请节点带来的性能消耗。
双向链表
双向链表就是双向带头循环链表,听起来很复杂,但是实际上双向链表的实现非常简单,并且双向链表的结构非常完美,可以实现在任意位置的O(1)的插入和删除。唯一的缺点就是不支持随机访问。
逻辑结构如图所示:
每个节点都有一个val数据域,两个指针域一个指向下一个节点,一个指向前一个节点。
注意:头节点的head的val不存放任何值,初始状态头节点的next和prev都是指向自己的。
有的代码用head的val存放节点个数,这个是不对的,如果val类型是double没法处理,如果是char那么上限就是255,万一节点多于255个呢?这些都是考虑不周的地方。
//dlist.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
LTDataType _val;
struct ListNode* _next;
struct ListNode* _prev;
}ListNode;
// 创建返回链表的头结点.
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
//dlist.c
#define _CRT_SECURE_NO_WARNINGS
#include"dlist.h"
//创建一个新节点
ListNode* buynewnode(LTDataType x)
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
node->_val = x;
node->_next = NULL;
node->_prev = NULL;
}
// 创建返回链表的头结点.
ListNode* ListCreate()
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
node->_next = node;
node->_prev = node;
return node;
}
// 双向链表销毁
void ListDestory(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->_next;
while (cur != pHead)
{
ListNode* next = cur->_next;
free(cur);
cur = next;
}
}
// 双向链表打印
void ListPrint(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->_next;
while (cur!=pHead)
{
printf("%d ", cur->_val);
cur = cur->_next;
}
}
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* tail = pHead->_prev;
ListNode* newnode = buynewnode(x);
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = pHead;
pHead->_prev = newnode;
}
// 双向链表尾删
void ListPopBack(ListNode* pHead)
{
assert(pHead);
assert(pHead->_next != pHead);//断言链表不为空
ListNode* tail = pHead->_prev;
ListNode* prev = tail->_prev;
prev->_next = pHead;
pHead->_prev = prev;
free(tail);
tail = NULL;
}
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* newnode = buynewnode(x);
newnode->_prev = pHead;
newnode->_next = pHead->_next;
pHead->_next->_prev = newnode;
pHead->_next = newnode;
}
// 双向链表头删
void ListPopFront(ListNode* pHead)
{
assert(pHead);
assert(pHead->_next != pHead);
ListNode* first = pHead->_next;
pHead->_next = first->_next;
first->_next->_prev = pHead;
free(first);
first = NULL;
}
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* cur = pHead->_next;
while (cur != pHead)
{
if (cur->_val == x)
return cur;
cur = cur->_next;
}
return NULL;
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* prev = pos->_prev;
ListNode* newnode = buynewnode(x);
newnode->_next = pos;
newnode->_prev = prev;
prev->_next = newnode;
pos->_prev = newnode;
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
assert(pos);
pos->_prev->_next = pos->_next;
pos->_next->_prev = pos->_prev;
free(pos);
pos = NULL;
}
双向链表的实现比较单链表简单了很多,因为少了很多边界条件的判断。
数组模拟双向链表
const int N = 100010;
int e[N],l[N],r[N],idx;
void init()
{
r[0] = 1;
l[1] = 0;
idx = 2;
}
void insert(int k,int x)
{
e[idx] = x;
l[idx] = k;
r[idx] = r[k];
l[r[k]] = idx;
r[k] = idx;
idx++;
}
void erase(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
双向链表这里使用了三个数组,e数组用来保存val值,l数组就是prev指针,r数组就是next指针。
在初始化中使用了两个数组位置来当作两个节点,左节点和右节点。这样就可实现在任意位置的O(1)复杂度的插入和删除。
insert实现的是在k的右边插入元素,如果想要在k的左边插入也很简单,在k的左边插入元素相当于在k的左边的元素的右边插入,所以可以一样的调用。