1. 顺序存储结构
顺序存储是存储线性表最简单的结构,具体做法是将线性表中的元素一个接一个地存储在一片相邻的存储区域中。这种顺序存储的线性表也被称为顺序表
顺序表具有一下两个基本特征:
1) 线性表中所有元素所占的存储空间是连续的
2) 线性表中各数据元素在存储空间中是按逻辑顺序依次存放的
在顺序表中,前件和后件两个元素在存储空间中是紧邻的,且前件元素一定存储在后件元素的前面。
#include<stdio.h>
#include<stdlib.h>
#define Maxsize = 10
typedef int Type;
// 定义线性表
typedef struct
{
Type * elem;
int len;
} SqList;
// 初始化
bool init_SqList(SqList *list)
{
list->len = 0;
list->elem = (Type*)malloc(sizeof(Type)*Maxsize);
if(list->elem == NULL)
{
return false;
}
return true;
}
// 输出线性表内容
void showList(SqList* list)
{
for (int i = 0; i < list->length; i++)
{
printf("%d ", l->elem[i]);
}
}
// 获取某个元素
int GetData(SqList *l,int index)
{
if(index<0 or index>l->len)
return false;
return l->elem[index];
}
// 插入数据
bool InsertData(SqList *l,int index,ElemType a)
{
if(l->len==MAXSIZE) // 判断表是否满
return false;
if(index<1 or index >l->len+1) // 判断表插入位置是否合理
return false;
if(index<=l->len)
{
for(int i=l->len;i>=index;i--)
{
l->elem[i]=l->elem[i-1];
}
l->len++; // 长度+1
l->elem[index-1]=a; // 插入新元素
}
return true;
}
// 删除指定数据
bool DeleteData(SqList *l,int index)
{
if(index<0 or index>l->len) // 判断表删除位置是否合理
return false;
for(int i=index-1;i<l->len-1;i++)
{
l->elem[i]=l->elem[i+1];
}
l->elem[l->len-1]=NULL; // 删除末尾元素
l->len--; // 长度-1
}
// 加载数据
void LoadSqList(SqList *l,int index)
{
for(int i=0;i<index;i++)
{
l->elem[i]=i+1;
l->len++;
}
}
int main(void)
{
SqList list;
IniSqList(&list);
LoadSqList(&list,7);
ShowSqList(&list);
printf("\n");
// InsertData(&list,1,9);
DeleteData(&list,1);
ShowSqList(&list);
getchar();
return 0;
}
2.链式存储结构
2.1 单向链表
线性表的链式存储单元的特点是用一组任意的存储单元存储线性表的数据元素,我们除了要存储它的元素信息外,我们还要存储它们的后继元素的存储地址。
如上图所示,在我们的节点中分数据域和指针域,指针域中存放的便是下一个结点的地址,如上图存放的0500便是下一个结点的地址。
对于线性表来说,总得有个头有个尾,链表也不例外。我们把链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。最后一个,当然就意味着直接后继不存在了,所以我们规定,线性链表的最后一个结点指针为“空”(通常用NULL或“^”符号表示)。
在这里插入图片描述
有时,我们为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息,也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。
头指针和头节点
头指针
1.头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
2.头指针具有标识作用,所以常用头指针冠以链表的名字
3.头指针是链表的必要元素
头节点1.头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度)
2.有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了
3.头结点不一定是链表必须要素
不带头节点的
带头节点的
带头节点的空表
本文采用的是带头结点的结构:
typedef int Type;
typedef struct Node
{
Type data;
struct Node* next;
}Node;
typedef Node* LinkList;
1)单链表的初始化
Linklist Init_linkList()
{
LinkList temp= (LinkList)malloc(sizeof(Node)); // 定义头结点以及指向头结点的指针
if (temp == NULL)
{
printf("malloc error \n");
}
// 初始化头结点
else
{
temp->data = 0;
temp->next = NULL;
}
return temp;
}
2)单链表的读取
我这里是通过其前驱的next指针(头节点的好处之一)来间接访问某个节点的data域,当然也可以直接指向该节点直接访问其数据域。
ELemType GetElem_Linklist(Linklist L, int i)//返回线性表中的第i个数据元素
{
assert(L != NULL && i >= 0 && i < L->data);
LinkList temp = L;
while (i != 0)
{
temp = temp->next;
i--;
}
return temp->next->data;
}
3)单链表的插入和读取
插入:
bool Insert_Linklist(Linklist L, int i, ELemType e)//在L线性表中的第i位插入e
{
assert(L != NULL && i >= 0 && i <= L->data);
Node* temp = (Node*)malloc(sizeof(Node));
if (temp == NULL)
{
return false;
}
else
{
L->data++;//先让个数+1,因为之后会修改L,此时不改就需要额外变量了
temp->data = e;
while (i != 0)//寻找i位置 这里循环可以定义一个临时的去表示L
{
L = L->next;
i--;
}
temp->next = L->next;//插入
L->next = temp;
return true;
}
}
单向链表的删除
bool Delete_Linklist(Linklist L, int i)//删除线性表L的第i个元素
{
assert(L != NULL && i >= 0 && i < L->data);
L->data--;
Node* pt = L->next;
while (i != 0)
{
L = L->next;
pt = pt->next;
i--;
}
L->next = pt->next;
free(pt);
return true;
}
其他接口:
int Length_Linklist(Linklist L)//返回线性表L中的元素个数
{
return L->data;
}
bool IsEmpty_Linklist(Linklist L)//线性表为空则返回true,否则false
{
assert(L != NULL);
return L->data == 0;
}
bool Clear_Linklist(Linklist L)//清空L;
{
while (!IsEmpty_Linklist(L))
{
Delete_Linklist(L, 0);
}
return true;
}
int LocateElem_Linklist(Linklist L, ELemType e)//在L线性表中查找与e相等的元素下标,没有则返回-1
{
assert(L != NULL);
int i = 0;
while (L->next != NULL && e != L->next->data)
{
L = L->next;
i++;
}
return L->next == NULL ? -1 : i;
}
2.2 静态链表
静态链表:分配一整片连续的内存空间,各个结点集中安置,逻辑结构上相邻的数据元素,存储在指定的一块内存空间中,数据元素只允许在这块内存空间中随机存放,这样的存储结构生成的链表称为静态链表。也就是说静态链表是用数组来实现链式存储结构,静态链表实际上就是一个结构体数组
举例:通过a[1]中存放的游标变量3可找到存放的数据元素5的后继元素为3,再通过a[3]中存放的游标变量2可找到存放数据元素3的后继数据为2,以此类推,直到某元素的游标变量为0即可停止(注:a[0]为头结点不存储数据元素)
备用链表:
在链表中,我们不可能恰好将所有的位置都使用,那么就会出现空闲的位置,用来连接这些空闲位置的链表,我们就将其称之为备用链表,他的作用为:回收数组中目前没有适用的空间。
那么此时就相当于有两个链表,实现不同的功能,一个用来连接数据,另一个用来连接数组中的空闲空间。
一般情况下,备用链表的表头位于数组下标为0(a[0])的位置,而数据链表的表头位于数据下标为1(a[1])的位置。
在这里插入图片描述如上图所示:备用链表的连接顺序依次是:a[0],a[2],a[4]数据链表的连接顺序依次是a[1],a[3],a[5]。
#include<stdio.h>
#include<stdlib.h>
#define initlen 10
typedef struct
{
char date;
int next;
}*NodePtr,Node;
typedef struct
{
NodePtr node;
int *used;//判断地址是否有值,1代表有值,0代表无值
}*list,lnode;
list initlist()
{
//temphead为指向整个静态链表的指针
list temphead=(list)malloc(sizeof(lnode)*initlen);
//将静态链表的空间分配给node数组和used数组
temphead->node=(NodePtr)malloc(sizeof(Node)*initlen);
temphead->used =(int*)malloc(sizeof(int)*initlen);
//初始化头节点
temphead->node->date='\0';
temphead->node->next=-1;
temphead->used[0]=1;//头节点不能用来存放数据
int i;
for(i=1;i<initlen;i++)
{
temphead->used[i]=0;
}
return temphead;
}
void printlist(list parahead)
{
int p=0;
while(p!=-1)
{
printf("%c",parahead->node[p].date );
p=parahead->node[p].next;
}
printf("\n");
}
void insert(list parahead,char ch,int position)
{
int i;
int p=0;//从头结点开始
//1、找到插入位置前的节点的下标
for(i=1;i<position;i++)
{
p=parahead->node[p].next;
if(p==-1)
{
printf("插入位置%d大于当前链表的长度\n",position);
return;
}
}
//2、查找当前链表中还有没有没用过的空间
int q=-1;
for(i=1;i<initlen;i++)
{
if(parahead->used[i]==0)
{
q=i;
parahead->used[q]=1;
break;
}
}
//3、如果没有多余的空间
if(q==-1)
{
printf("链表已满,不能插入元素\n");
return;
}
parahead->node[q].date =ch;
parahead->node[q].next=parahead->node[p].next ;
parahead->node[p].next=q;
}
void deletenode1(list parahead,int position)
{
int i,p=0;
for(i=1;i<position;i++)
{
p=parahead->node[p].next ;
if(p==-1)
{
printf("删除位置大于当前链表的实际长度\n");
return;
}
}
int q=parahead->node[p].next ;
parahead->node[p].next =parahead->node[q].next ;
parahead->used[q]=0;
}
//删除指定元素的节点
void deletenode2(list parahead,char ch)
{
int p=0;
while(parahead->node[p].next !=-1&¶head->node[parahead->node[p].next ].date!=ch)
{
p=parahead->node[p].next ;
}
if(parahead->node[p].next ==-1)
{
printf("链表中没有%c,无法删除\n",ch);
return;
}
int q=parahead->node[p].next ;
parahead->node[p].next =parahead->node[q].next ;
parahead->used[q]=0;
}
//测试
void test()
{
list tempList = initlist();
printlist(tempList);
//插入元素
insert(tempList, 'H', 1);
insert(tempList, 'e', 2);
insert(tempList, 'l', 3);
insert(tempList, 'l', 4);
insert(tempList, 'o', 5);
printlist(tempList);
//删除元素
printf("Deleting 'e'.\r\n");
deletenode2(tempList, 'e');
printf("Deleting 'a'.\r\n");
deletenode2(tempList, 'a');
printf("Deleting 'o'.\r\n");
deletenode2(tempList, 'o');
printlist(tempList);
printf("删除第2个元素\r\n");
deletenode1(tempList,2);
printf("删除第1个元素\r\n");
deletenode1(tempList,1);
printlist(tempList);
insert(tempList, 'x', 1);
printlist(tempList);
}
int main()
{
test();
return 0;
}
2.3 循环链表
循环链表:和单链表唯一的区别:尾节点保留着头节点的地址。
循环链表的特点:
1. 尾节点可以找到头节点
2. 头节点不能找到尾节点
3. 如果没有有效数据节点(空链),头节点指向自身
注意,循环链表,for循环去遍历,判断表达式不再是p->next!=NULL,而是p->next!=plist
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include "clist.h"
/*
注意事项:
1.尾可以找到头,但是头不能找到尾
2.如果循环链表是一个空链,头结点的指针域指向自身
3.注意,循环链表,for循环去遍历,判断表达式不再是p->next!=NULL,而是p->next!=plist
*/
/*
for(struct Node* p=plist; p->next!=NULL; p=p->next)//上面for循环用于需要前驱的操作:比如插入删除
for(struct Node* p=plsit->next; p!=NULL; p=p->next)//下面for循环用于不需要前驱的操作:比如查找,找有效值个数,打印
*/
//有关循环链表的操作函数:
//初始化
void Init_clist(struct CNode* plist)
{
//assert
//plist->data; 数据域不需要处理
//plist->next = NULL;//单链表的写法
plist->next = plist;//循环链表的写法
}
//购买节点
struct CNode* BuyNode(ELEM_TYPE val)
{
//assert
struct CNode* pnewnode = (struct CNode*)malloc(1 * sizeof(struct CNode));
assert(pnewnode != NULL);
if(pnewnode == NULL)
{
return NULL;
}
pnewnode->data = val;
pnewnode->next = NULL;
return pnewnode;
}
//头插
bool Insert_head(PCNode plist, ELEM_TYPE val)
{
//assert
//1.购买新节点
struct CNode *pnewnode = BuyNode(val);
assert(pnewnode != NULL);
if(pnewnode == NULL)
{
return false;
}
//2.找到合适的插入位置 头插不需要特意去找合适的插入位置
//3.插入
pnewnode->next = plist->next;
plist->next = pnewnode;
return true;
}
//尾插
bool Insert_tail(PCNode plist, ELEM_TYPE val)
{
//assert
//1.购买新节点
struct CNode *pnewnode = BuyNode(val);
assert(pnewnode != NULL);
if(pnewnode == NULL)
{
return false;
}
//2.找到合适的插入位置
struct CNode* p;
for(p=plist; p->next!=plist; p=p->next);//用上面的for循环 让p停留在尾结点
//此时 p指向尾结点
//3.插入
pnewnode->next = p->next;
p->next =pnewnode;
return true;
}
//按位置插 pos=0 头插
bool Insert_pos(PCNode plist, int pos, ELEM_TYPE val)
{
assert(plist != NULL);//保证循环链表存在
assert(pos >=0 && pos<=Get_length(plist));
//1.购买新节点
struct CNode *pnewnode = BuyNode(val);
assert(pnewnode != NULL);
if(pnewnode == NULL)
{
return false;
}
//2.找到合适的插入位置
struct CNode *p = plist;
for(int i=0; i<pos; i++)
{
p=p->next;
}
//此时 p指向待插入位置的前一个节点
//3.插入
pnewnode->next = p->next;
p->next = pnewnode;
return true;
}
//头删
bool Del_head(PCNode plist)
{
//assert
//删除需要判空
if(Is_Empty(plist))
{
return false;
}
struct CNode *p = plist->next;
plist->next = p->next;//plist->next = plist->next->next;
free(p);
return true;
}
//尾删
bool Del_tail(PCNode plist)
{
//assert plist!=NULL
if(Is_Empty(plist)) //如果不空,那至少有一个有效数据
{
return false;
}
//让p和q找到合适的位置:
/*//第一种方案:先找p,再找q
struct CNode *p = plist;
for(p; p->next!=plist; p=p->next);//此时for循环结束,p指向尾结点
struct CNode *q= plist;
for(q; q->next!=p; q=q->next);//此时for循环结束,q在p前面
*/
//第二种方案:先找q,再直接让p=q->next即可
struct CNode *q = plist;
for(q; q->next->next!=plist; q=q->next);//q-> 和 q->next-> 没有风险,前面排除过了
//此时for循环结束,q在倒数第二个节点
struct CNode *p = q->next;
//跨越指向,再接着释放待删除节点
q->next = p->next;//q->next = q->next->next;
free(p);
return true;
}
//按位置删 pos=0 头删
bool Del_pos(PCNode plist, int pos)
{
assert(plist != NULL);
assert(pos >=0 && pos<Get_length(plist));
if(Is_Empty(plist))
{
return false;
}
//让p和q找到合适的位置:
struct CNode *q = plist;
for(int i=0; i<pos; i++)
{
q = q->next;
}
struct CNode *p = q->next;
//跨越直线,释放待删除节点
q->next = p->next;
free(p);
return true;
}
//按值删除
bool Del_val(PCNode plist, ELEM_TYPE val)
{
//assert
if(Is_Empty(plist))
{
return false;
}
struct CNode *p = Search(plist, val);
if(p == NULL)
{
return false;
}
//此时,如果p不为NULL,代表val值存在,且p现在指向它
//在让q找到p的前一个节点
struct CNode *q = plist;
for(q; q->next!=p; q=q->next);
//跨越指向,释放待删除节点
q->next = p->next;
free(p);
return true;
}
//查找
struct CNode* Search(PCNode plist, ELEM_TYPE val)
{
//assert
for(struct CNode *p=plist->next; p!=plist; p=p->next)
{
if(p->data == val)
{
return p;
}
}
return NULL;
}
//判空
bool Is_Empty(PCNode plist)
{
return plist->next == plist;
}
//判满 循环链表也不需要判满
//获取有效元素个数
int Get_length(PCNode plist)
{
//assert
int count = 0;
for(struct CNode *p=plist->next; p!=plist; p=p->next)
{
count++;
}
return count;
}
//清空
void Clear(PCNode plist)
{
Destroy(plist);
}
//销毁1(借助头结点,无限头删)
void Destroy(PCNode plist)
{
//assert
while(plist->next != plist)
{
struct CNode *p = plist->next;
plist->next = p->next;
free(p);
}
plist->next = plist;
}
//销毁2(不借助头结点,有两个临时指针)
void Destroy2(PCNode plist)
{
//assert plsit!=NULL
struct CNode *p = plist->next;
struct CNode *q = NULL;
plist->next = plist;
while(p != plist)
{
q = p->next;
free(p);
p = q;
}
}
//打印
void Show(PCNode plist)
{
//assert
for(struct CNode *p=plist->next; p!=plist; p=p->next)
{
printf("%d ", p->data);
}
printf("\n");
}
2.4 双向链表
双向链表叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
typedef struct DoubleLinkedNode{
char data;
struct DoubleLinkedNode *previous;
struct DoubleLinkedNode *next;
}DLNode, *DLNodePtr;
DLNodePtr initLinkList(){
DLNodePtr tempHeader = (DLNodePtr)malloc(sizeof(struct DoubleLinkedNode));
tempHeader->data = '\0';
tempHeader->previous = NULL;
tempHeader->next = NULL;
return tempHeader;
}
打印链表
void printList(DLNodePtr paraHeader){
DLNodePtr p = paraHeader->next;
while(p != NULL){
printf("%c",p->data);
p = p->next;
}
printf("\r\n");
}
链表的插入
将p指向需要插入位置的前一位,新建一个q指向需要插入的链表,将q的后继指针指向p的后继指针指向的链表r,q的指针前驱指向p,p的后继指针指向q,如果r不为空,r的前驱指向q,q就成功地插入到了p和r的中间。
void insertElement(DLNodePtr paraHeader,char paraChar,int paraPosition){
DLNodePtr p,q,r;
p = paraHeader;
for(int i = 0;i < paraPosition;i++){
p = p->next;
if(p == NULL){
printf("The position %d is beyond the scope of the list.",paraPosition);
return;
}
}
q = (DLNodePtr)malloc(sizeof(struct DoubleLinkedNode));
q->data = paraChar;
r = p->next;
q->next = p->next;
q->previous = p;
p->next = q;
if(r != NULL){
r->previous = q;
}
}
删除指定元素
将p指向头指针,将p依次向后移动,直到p的后继等于空,如果p的后继为空的话,就输出无法删除,或者p的后继中的元素是想要删除的元素,就将p的后继令为q,q中的元素就是想要删除的元素,r是q的后继,将p的后继指针指向r,如果r不为空的话,将r的前驱指向p,q中的元素就被删除了。
void deleteElement(DLNodePtr paraHeader,char paraChar){
DLNodePtr p,q,r;
p = paraHeader;
while((p->next != NULL)&&(p->next->data != paraChar)){
p = p->next;
}
if(p->next == NULL){
printf("The char '%c' does not exist.\r\n",paraChar);
return;
}
q = p->next;
r = q->next;
p->next = r;
if(r != NULL){
r->previous = p;
}
free(q);
}
查找元素的位置
void locateElement(DLNodePtr paraHeader,char paraChar){
DLNodePtr p;
int i=0;
p = paraHeader;
while((p->next != NULL)&&(p->next->data != paraChar)){
p = p->next;
i++;
}
if(p->next == NULL){
printf("The char '%c' is Nofound\n",paraChar);
}
else{
printf("Location of '%c' is '%d'\n",paraChar,i);
}
}
双向链表可以找到前驱和后继,可进可退,但是相对单链表需要多分配一个指针存储空间。