一、链表的几种结构
1.单向或者双向链表
2.带头或者不带头(哨兵位的头结点,不存储有效数据)
3.循环或者非循环
实际中最常用的是下面这两种:
①无头单向非循环链表
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等,另外这种结构在笔试面试中出现很多。
②带头双向循环链表
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
二、带头双向循环链表实现代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int LTDateType;
struct ListNode
{
LTDateType data;
struct ListNode* next;
struct ListNode* prev;
}ListNode;
LTNode* ListInit();
void ListDestory(LTNode* phead);
void ListPrint(LTNode* phead);
void ListPushBack(LTNode* phead,LTDateType x);
void ListPushFront(LTNode* phead,LTDateType x);
void ListPopBack(LTNode* phead);
void ListPopFront(LTNode* phead);
LTNode* ListFind(LTNode* phead,LTDateType x);
void ListInsert(LTNode* pos,LTDateType x);
void ListErase(LTNode* pos);
#include "List.h"
void TestList1()
{
LTNode* plist=ListInit();
ListInit(plist);
ListPushFront(plist,1);
ListPushFront(plist,2);
ListPushFront(plist,3);
ListPushFront(plist,4);
ListPrint(plist);
ListPushBack(plist,1);
ListPushBack(plist,2);
ListPushBack(plist,3);
ListPushBack(plist,4);
ListPrint(plist);
LTNode* pos=ListFind(plist,2);
if(pos)
{
ListErase(pos);
}
ListPrint(plist);
ListPopBack(plist,1);
ListPopBack(plist,2);
ListPopFront(plist,3);
ListPopFront(plist,4);
ListPrint(plist);
ListDestory(plist);
plist=NULL;
}
int main()
{
TestList1();
return 0;
}
#include "List.h"
//创建新结点,存放X的值
LTNode* BuyListNode(LTDateType x)
{
LTNode* newnode=(LTNode*)malloc(sizeof(LTNode));//创建一个新结点
newnode->data=x;//存放x的值
newnode->next=NULL;
newnode->pre=NULL;
return newnode;
}
//链表初始化
LTNode* ListInit()
{
//哨兵位头结点,哨兵位的头结点不存放数值
LTNode* phead=(LTNode*)malloc(sizeof(LTNode));
phead->next=phead;
phead->prev=phead;//只有一个头结点
return phead;
}
//打印
void ListPrint(LTNode* phead)
{
assert(phead);
LTNode* cur=phead->next;
while(cur!=phead)
{
printf("%d",cur->data);//打印
cur=cur->next;//指向下一个结点,直到返回头结点
}
printf("\n");
}
//尾插法
void ListPushBack(LTNode* phead,LTDateType x)
{
assert(phead);
LTNode* tail=phead->prev;
LTNode* newnode=(LTNode*)malloc(sizeof(LTNode));//创建一个新结点
newnode->data=x;//存放x的值
tail->next=newnode;
newnode->prev=tail;//建立tail和newnode的关系
newnode->next=phead;
phead->prev=newnode;//建立phead和newnode的关系
}
//尾删法,删的是phead的prev
void ListpopBack(LTNode* phead)
{
assert(phead);
assert(phead->next!=phead);
LTNode* tail=phead->prev;//定义尾结点
LTNode* pre=tail->prev;//定义尾结点的前驱
pre->next=phead;
phead->next=prev;//尾结点前驱和头结点的关系
free(tail);//释放尾结点
}
//头插法
void ListPushFront(LTNode* phead,LTDateType x)
{
assert(phead);
LTNode* newnode=BuyListNode(x);
LTNode* next=phead->next;//头结点的后继结点
phead->next=newnode;
newnode->prev=phead;//新结点和头结点的关系
newnode->next=next;
next->prev=newnode;//头结点后继和新结点的关系
}
//头删法
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next!=phead);
LTNode* next=phead->next;
LTNode* nextNext=next->next;
phead->next=nextNext;
nextNext->prev=phead;
free(next);
}
//查找
LTNode* ListFind(LTNode* phead,LTDateType x)
{
assert(phead);
LTNode* cur=phead->next;
while(cur!=phead)
{
if(cur->data=x)
{
return cur;//找到,返回cur
}
cur=cur->next;//指向下一个结点
}
return NULL;//找不到,返回NULL
}
//pos位置之前插入
void ListInsert(LTNode* pos,LTDateType x)
{
assert(pos);
LTNode* prev=pos->prev;
LTNode* newnode=BuyListNode(x);
prev->next=newnode;//插入新结点
newnode->prev=prev;
newnode->next=pos;
pos->prev=newnode;//定义新结点的前驱后继关系
}
//删除pos位置
void ListErase(LTNode* pos)
{
assert(pos);
LTNode* prev=pos->prev;//定义pos的前驱prev
LTNode* next=pos->next;//定义pos的后继next
prev->next=next;//前驱的下一个结点指向next
next->prev=prev;//后继的前驱变为prev
}
//删除链表
void ListDestory(LTNode* phead)
{
assert(phead);
LTNode* cur=phead->next;
while(cur!=phead)
{
LTNode* next=cur->next;
free(cur);
cur=next;//往下走
}
free(phead);
phead=NULL;//传一级指针的话,置空不起作用
}
//传二级指针的原因,一级指针会出现野指针的危害
三、顺序表和链表
顺序表
优点:
1.支持随机访问,需要随机访问结构支持算法可以很好的适用。 2.CPU高速缓存命中率更高。
缺点:
1.头部中部插入删除时间效率低。0(N)
2.连续的物理空间,空间不够了以后需要增容。
a、增容有一定程度程度消耗。
b.为了避免频繁增容,一般我们都按倍数去增,
用不完可能存在一定的空间浪费
链表(双向带头循环链表)
优点:
1、任意位置插入删除效率高。O(1)
2、按需申请释放空间。
缺点:
1.不支持随机访问。(用下标访问)意味着:一些排序,二分查找等在这种结构上不适用。
2.链表存储一个值,同时要存储链接指针,也有一定的消耗。 3.高速缓存的命中率更低。