链表
单链表
每个节点除去存放数据元素外,还需存储指向下一个节点的指针
定义
struct LNode{
ElementType data;
struct LNode *next;
};
//增加一个新的节点,在内存中申请新的节点所需要的空间,并用指针P指向这个节点
struct Lnode *p = (struct LNode *)malloc(sizeof(struct LNode));
//用typedef实现结构体的重命名,方便声明结构体类型的节点
typedef struct LNode LNode;
typedef struct Lnode *LinkList;
//后续声明只需要用LNode来代替struct LNode
LNode *p = (LNode *)malloc(sizeof(LNode));
//上面两步也可以合并来完成
typedef struct LNode{
EleType data;
struct LNode *next;
}LNode,*LinkList;
//其中LNode是结构体类型,LinkList是指针类型
Lnode *p = (LNode *)malloc(sizeof(LNode));
LinkList q = (LinkList)malloc(sizeof(Lndoe));
LNode和LinkList本质上只是结构体类型和结构体的指针类型,但在使用时可以用LNode表示节点,用LinkList表示单链表更容易逻辑清晰。
例如在实现获取链表中第i个元素的函数中
//返回类型为LNode *突出返回的是一个节点
//入参类型LintList强调传入的是单链表
LNode *GetElementInLinkList(LinkList L,int i){
if(i < 1)
return NULL;
else if(i = 1)
return L;
else
{
LNode *p = L;
for(int j = 0;j < i&&p!=NULL;j++){
p = p -> next;
}
return p;
}
}
实现
不带头节点
typedef struct LNode{
int data;
struct LNode *next;
}LNode,*LinkList;
//初始化一个空的链表
bool InitList(LinkList &L){
L = NULL;
return true;
}
带头节点
带头结点可以用相同的代码逻辑处理第一个数据点和后续的数据点
typedef struct LNode{
int data;
struct LNode *next;
}Lnode, *LinkList;
//初始化一个单链表(带头节点)
bool InitList(LinkList &L){
L = (LNode)malloc(sizeof(LNode));
if(L == NULL)
return false;//malloc分配内存空间失败
L -> next = NULL;
return ture;
}
基本操作的实现
插入
按位序插入
- 带头节点
- 不带头节点
在第i个位置插入指定的元素e
//带头节点
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
bool ListInsert(LinkList &L,int i,int e){
LNode *p;
p = L;//p指向L指向的地址
if(i < 1)
return false;
for(int j = 0;j < i - 1 && L != NULL;j++){
p = p -> next;
}
if(p == NULL)
return false;
LNode *q =(LNode)malloc(sizeof(Lnode));
q -> data = e;
q -> next = p -> next;
p -> next = q;
return true;
}
上述代码没有改变L的指向,传入指针的地址看起来是危险且复杂的,做如下修改
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
bool ListInsert(LinkList p,int i,int e){
if(i < 1)
return false;
for(int j = 0;j < i - 1 && L != NULL;j++){
p = p -> next;
}
if(p == NULL)
return false;
LNode *q =(LNode)malloc(sizeof(Lnode));
q -> data = e;
q -> next = p -> next;
p -> next = q;
return true;
}
这样做减少了用到的变量,且不用进行结构体指针的地址运算。
时间复杂度非常容易得出
最小O(1)
最大O(n)
平均O(n)
当不带头节点时,NodeList的指向是可能发生变化的,所以需要传入指针的地址改变其指向
//不带头节点
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
bool ListInsert(LinkList &L,int i,int e){
if(i == 1){
LNode *h = (LNode *)malloc(sizeof(LNode));
if(h == NULL)
return false;
h -> data = e;
h -> next = L;
&L = &h;
return true;
}
LNode *p;
p = L;//p指向L指向的地址
if(i < 1)
return false;
for(int j = 0;j < i - 1 && L != NULL;j++){
p = p -> next;
}
if(p == NULL)
return false;
LNode *q =(LNode *)malloc(sizeof(Lnode));
if(q ==NULL)
return false;
q -> data = e;
q -> next = p -> next;
p -> next = q;
return true;
}
指定节点的后插操作
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
//在p节点后插入
bool InsertNextNode(LNode *p,int e){
if(p == NULL)
return false;
LNode *s = (LNode *)(malloc(sizeof(LNode));
s -> data = e;
s -> next = p -> next;
p -> next = s;
return true;
}
//
指定节点的前插操作
如果用在节点的前驱节点后插入的想法去实现,就会使实现非常困难,在此用到一个非常巧妙的方法,值交换
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
//在p节点前插入(先后插再交换值)
bool InsertNextNode(LNode *p,int e){
if(p == NULL)
return false;
LNode *s = (LNode *)(malloc(sizeof(LNode));
s -> next = p ->next;
p -> next = s;
s -> value = p;
p -> value = e;
return true;
}
删除
按位序删除
//按位序删除(带头节点)
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
bool Delete(LinkList L,int i,int &e){
if(i < 1)
return false;
for(int j = 0; j < i - 1 && L != NULL; j++){
L = L->next;
}
if( L == NULL)
return false;
LNode p = (LNode *)malloc(sizeof(LNode));
p = L -> next;
if( p == NULL)
return false;
e = p -> data;
L -> next = p -> next;
free(p);
return true;
}
指定节点的删除
typedef struct LNode{
ElementType data;
struct LNode;
}LNode,*LinkList;
bool Delete(LNode *p){
LNode *q = p -> next;
if(*q == NULL){
free(p);
return true;
}
p -> data = q -> data;
p -> next = q -> next;
free(q);
return true;
}
查找
按位查找
typedef struct LNode{
elementType data;
struct LNode *next;
}LNode, *LinkList;
//按位查找,返回第i位元素(带头节点)
LNode *GetElem(LinkList L,int i){
if( i < 1 )
return NULL;
for(int j = 0;j < i - 1 && L != NULL;j++){
L = L -> next;
}
return L;
}
按值查找
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
LNode *GetElem(LinkList L,int e){
for(;L != NULL&& L -> data != e;L = L->next);
return L;
}
求表长
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
//带头节点
int Length(LinkList L){
int len = 0;
for(;L -> next != NULL;len ++);
return len;
}
建立
尾插法
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
//带头节点,初始化
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode));
if(L == NULL)
return false;
L -> next = NULL;
return true;
}
//后插法带头节点不初始化
LinkList ListTailInsert(LinkList L){
LNode *head = L;
LNode *p = (LNode *)malloc(sizeof(LNode));
p -> next = NULL;
int x;
scanf("%d",&x);
while(x!=9999){
LNode *p = (LNode *)malloc(sizeof(LNode));
p -> data = x;
L -> next = p;
L = p;
scanf("%d",&x);
}
return head;
}
头插法
//头插法带头节点带初始化
LintList ListHeadInsert(){
LNode *L = (LNode *)malloc(sizeof(LNode));
L -> next = NULL;
int x;
scanf("%d",&x);
while(x != 9999){
LNode *p = (LNode *)malloc(sizeof(LNode));
p -> data = x;
p -> next = L -> next;
L -> next = p;
scanf("%d",&x);
}
return L;
}
头插法的应用,列表的逆转
typedef truct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
LinkList reverOrder(LinkList oriList){
LinkList L = (LinkList) malloc(sizeof(LNode));
p -> value =
L -> next = null;
oriList = oriList -> next;
while(oriList != NULL){
LNode *p = (LNode *)malloc(sizeof(LNode));
p -> value = oriList -> value;
p -> next = L -> next;
L -> next = p;
oriList = oriList -> next;
}
return L;
}
双链表
每个节点有前驱和后继
typedef struct DNode{
ElemType data;
struct LNode *prior,*next;
}DNode, *DLinkList;
初始化
//有头节点
typedef struct DNode{
ElemType data;
struct Lnode *prior,*next;
}DNode, *DLinkList;
bool InitDLL(DLinkList &L){
L = (DNode *)malloc(sizeof(DNode));
if(L == NULL)
return false;
h -> prior = NULL;
h -> next = NULL;
return ture;
}
//判空
bool Empty(DLinkList L){
if(L -> next == NULL)
return ture;
return false;
}
插入
//在p节点后插入s节点
bool InsertNextDNode(DNode *p,DNode *s){
if(p == NULL && s == NULL)
return false;
if( p -> next != NULL)
p -> next -> prior = s;
s -> next = p -> next;
s -> prior = p;
p -> next = s;
return true;
}
删除
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,DLinkList;
//删除p节点的后继节点
bool DeleteNextDNode(DNode *p){
if( p == NULL )return false;
if( p -> next == NULL)return false;
DNode *d = p -> next;
p -> next = d -> next;
if( d -> next != NULL)
d -> next -> prior = p;
free(d);
return true;
}
//销毁表
void Destroy(DLinkList &L){
while(L -> next != NULL)
DeleteNextDNode(L);
free(L);
L = NULL;
}
遍历
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,DLinkList;
bool NextTraver(DNode *p){
if( p == NULL|| p -> next == NUL)return false;
while(p -> next != NULL){
p = p-> next;
}
return true;
}
bool PriorTraver(DNode *p){
if(p == NULL || p -> next == NULL || p -> next -> next == NULL)return false;
while(p -> front -> front != NULL){
p = p-> prior;
}
return true;
}
循环链表
循环单链表
typedef struct LNode{
ElemType data;
struct LNode *next;
} LNode,*LinkList;
//初始化一个循环单链表
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode));
if( L == NULL)return false;
L -> next = L;
return true;
}
//判断表尾
bool IsTail(LinkList L,LNode *p){
if( p -> next == L)return true;
return false;
}
循环双链表
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}
//初始化循环双链表
bool InitDLL(DLinkList &L){
L = (DNode *)malloc(sizeof(DNode));
if(L == NULL)return false;
L -> prior = L;
L -> next =L;
return true;
}
//判空
bool Empty(DLinkList L){
if(L -> next == L)return true;
return false;
}
//判断表尾
bool IsTail(DLinkList L,DNode *p){
if(p -> next == L)return true;
return false;
}
//在p后插入s
bool InsertCirDLL(DNode *p,DNode *s){
if(p == NULL||s == NULL)return false;
p -> next -> prior = s;
}
静态链表
单链表:各个节点在内存中星罗棋布,散落天涯。
静态链表:分配一整片连续的内存空间,各个节点集中安置(顺序表?)
静态链表的节点:
- 数据项
- 下一个元素的下标
定义及基本操作
#define MaxSize 10 //静态链表的最大长度
typedef struct { //静态链表结构类型的定义
Element data; //存储数据元素
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
SLinkList L;
//初始化(head->next设为-1 el -> next设为-2)
bool Init(SLinkList L){
if( L == NULL)return false;
L[0] -> next = -1;
for (int i = 1;i < MaxSize ;i++)
L[i] -> next = -2;
return ture;
}
顺序表和链表的比较
逻辑结构
都是线性表,都是线性结构
物理结构
- 顺序表
顺序存储
优点:支持随机存取,存储密度高
缺点:大片连续空间分配不方便,改变容量不方便 - 链表
链式存储
优点:离散的小空间分配方便,改变容量方便
缺点:不可随机存取,存储密度低
数据的运算/基本操作
创销、增删改查
创建
- 顺序表
需要分配一大片连续空间(数组) - 链表
只需分配一个头节点,之后方便扩展
静态分配:静态数组(容量不可变)
动态分配:动态数组(malloc、free)容量可改变,但需要移动大量元素,时间成本高
销毁
- 顺序表
修改length = 0
静态分配:系统自动回收
动态分配:需要手动free - 链表
依次删除各个节点(free)
增删
- 顺序表
时间复杂度O(n),时间开销来自于移动每一项后续的数据元素,如果数据元素很大,则开销巨大 - 链表
时间复杂度O(N),时间开销来自于查找前驱元素,查找后只需要修改指针即可。
改查
按位查找
- 顺序表
时间复杂度O(1) - 链表
时间复杂度O(n)
按值查找
- 顺序表
遍历:时间复杂度O(n)
若表内元素有序,可以二分查找
二分查找:时间复杂度O(log(2,n)) - 链表
时间复杂度:O(n)
选择
- 顺序表
表长可预估,查询搜索操纵较多(增删操作较少) - 链表
表长难以估计,经常增删元素
答题模板
问题:巴拉巴拉,用顺序表还是链表好?
顺序表和链表的逻辑结构都是线性结构,都属于线性表。
但是两者的存储结构不同,线性表采用顺序存储,支持随机存取,存储密度高,但大片连续内存空间分配不方便,扩容不方便;链表采用链式存储,离散的小空间分配方便,扩容方便,但不可随机存取,存储密度低。
由于采用不同的存储方式实现,因此基本操作的实现效率不同。当初始化时…,当插入数据元素时…,当删除一个数据元素时…,当查找一个数据元素时…。