线性表:是n个具有相同特性的数据元素的有序排列
一、顺序表
顺序表:用一段物理地址连续的存储单元一次存储数据元素的线性结构,一般情况下采用数组存储
顺序表头文件
#pragma once
#include<stdio.h>
#include<assert.h>
//void assert( int expression );
//assert 的作用是现计算表达式 expression ,如果其值为假(即为0),那么它先向 stderr 打印一条出错信息,然后通过调用 abort 来终止程序运行。
//使用 assert 的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。
#include<stdlib.h>//包含malloc,calloc与free
typedef int SLDataType;
//动态顺序表
typedef struct SeqList
{
SLDataType* a;
size_t size;
size_t capacity;//容量,最大数组存储量,为数据个数
}SL;
//动态顺序表的缺陷:
//1.如果空间不够,增容会有一定的性能消耗,可能会有空间浪费
//2.在头部或者中部的插入删除效率低,为O(n)
//静态顺序表,数组给少不够用,给多会浪费
//#define N 1000
//typedef int SLDataType;
//typedef struct SeqList
//{
// SLDataType array[N];
// int size;
//}SL;
//接口函数
void SeqListInit(SL* ps);//初始化顺序表
void SeqListDestory(SL* ps);//销毁顺序表
void CheckCapacity(SL* ps);
void SeqListPushBack(SL* ps, SLDataType x);//尾插法
void SeqListPopBack(SL* ps);//尾删
void SeqListPushFront(SL* ps, SLDataType x);//头插
void SeqListPopFront(SL* ps);//头删
int SeqListFind(const SL* ps, SLDataType x);//查找指定值
void SeqListPrint(const SL* ps);//打印顺序表
void SeqListInsert(SL* ps, size_t pos,SLDataType x);//在pos未知插入数据x
void SeqListErase(SL* ps, size_t pos);//删除pos位置元素
void SeqListSize(const SL* ps);//查看顺序表元素个数
void SeqListAt(SL*, size_t pos, SLDataType x);//修改pos位置数据为x
顺序表源文件:
#include"SeqList.h"
//接口函数的实现
//初始化顺序表
void SeqListInit(SL *ps)
{
ps->a = NULL;//初始顺序表为空
ps->size = 0;//初始数据个数为0
ps->capacity = 0;//初始空间容量为0
}
//销毁顺序表
void SeqListDestory(SL* ps)
{
assert(ps != NULL);
free(ps->a);//释放动态开辟空间
ps->a = NULL;//置空
ps->size = 0;
ps->capacity = 0;
}
//检查顺序表容量是否满,增容
//为什么不采取插一个数据,增容一个空间的方式呢,因为这样也太麻烦了,代价也太大了
//一般情况下,为了避免频繁的增容,当空间满了后,我们不会一个一个的去增,而是一次增容 2 倍,当然也不会一次增容太大,比如 3 倍 4 倍,空间可能会浪费,2 倍是一个折中的选择。
void CheckCapacity(SL*ps)
{
assert(ps!=NULL);
if (ps->size==ps->capacity)
{
int newcapacity;//设置新容量
if (ps->capacity==0)
{
newcapacity = ps->capacity = 4;//原容量为0,扩容为4
}
else
{
newcapacity = 2 * ps->capacity;//原容量不为0,扩容为原来二倍
}
SLDataType* p = (SLDataType*)realloc(ps->a, newcapacity * sizeof(SLDataType));
//C 库函数 void *realloc(void *ptr, size_t size) 尝试重新调整之前调用 malloc 或 calloc 所分配的 ptr 所指向的内存块的大小。
//ptr -- 指针指向一个要重新分配内存的内存块,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的。如果为空指针,则会分配一个新的内存块,且函数返回一个指向它的指针。
//size -- 内存块的新的大小,以字节为单位。如果大小为 0,且 ptr 指向一个已存在的内存块,则 ptr 所指向的内存块会被释放,并返回一个空指针。
//该函数返回一个指针 ,指向重新分配大小的内存。如果请求失败,则返回 NULL。
if (p==NULL)
{
perror("realloc");
exit(-1);
}
else
{
ps->a = p;//p不为空,开辟成功
ps->capacity = newcapacity;//更新容量
}
}
}
//顺序表尾插
void SeqListPushBack(SL* ps,SLDataType x)
{
assert(ps != NULL);
CheckCapacity(ps);//检查顺序表容量是否已满
ps->a[ps->size] = x;
ps->size++;
}
//顺序表尾删
void SeqListPopBack(SL*ps)
{
assert(ps != NULL);
assert(ps->size > 0);
ps->size--;//已储存数据个数减一
//不知道SLDataType是什么数据类型,不能贸然赋值为0
//ps->a[ps->size-1]=0;
}
//顺序表头插
void SeqListPushFront(SL *ps,SLDataType x)
{
assert(ps);
CheckCapacity(ps);
int i = 0;
for (i=ps->size;i>=0;i--)//顺序表中[0,size-1]的元素依次向后挪动一位
{
ps->a[i + 1] = ps->a[i];
}
ps->a[0] = x;
ps->size++;
}
//顺序表头删
void SeqListPopFront(SL* ps)
{
assert(ps); //断言
assert(ps->size > 0); //顺序表不能为空
int i = 0;
for (i = 1; i < ps->size; i++) //顺序表中[1,size-1]的元素依次向前挪动一位
{
ps->a[i - 1] = ps->a[i];
}
ps->size--; //有效数据个数-1
}
//打印顺序表
void SeqListPrint(const SL* ps)
{
assert(ps != NULL);
if (ps->size==0)
{
printf("顺序表为空\n");
return;
}
for (int i = 0;i<ps->size;i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
//在顺序表中查找某值
int SeqListFind(const SL*ps,SLDataType x)
{
assert(ps);
for (int i=0;i<ps->size;i++)
{
if (ps->a[i] == x)
{
return i;//查找到,返回该值在数组中的下标
}
}
return -1;//未找到
}
//在顺序表指定下标位置插入数据
//
//int i = 0;
//for (i = ps->size - 1; i >= pos; i--)
//{
// ps->a[i + 1] = ps->a[i];
//}
//这种写法,当顺序表为空size=0时,i=-1,执行i>=pos时,i会被算数转化成无符号数
//无符号数-1为一个很大值的正数,远大于pos,满足条件进入循环,会造成越界访问
void SeqListInsert(SL* ps, size_t pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);//检查pos下标合法性
CheckCapacity(ps);
int i = 0;
for (i=ps->size;i>pos;i--)//将pos位置后面的数据依次向后移位
{
ps->a[i] = ps->a[i - 1];
}
ps->a[pos] = x;
ps->size++;
}
//顺序表中删除指定下标位置数据
void SeqListErase(SL* ps,size_t pos)
{
assert(ps);
assert(ps->size > 0);
assert(pos >= 0 && pos < ps->size);
int i = 0;
for (i = pos + 1; i < ps->size; i++)//将pos位置后面的数据一次向前移位
{
ps->a[i - 1] = ps->a[i];
}
ps->size--;
}
//查看顺序表中有效数据的个数
void SeqListSize(SL* ps)
{
assert(ps);
return ps->size;
}
//在数据结构中有一个约定,如果要访问或修改数据结构中的数据,不要直接去访问,要去调用它的函数来访问和修改,这样更加规范安全,也更方便检查是否出现了越界等一些错误情况
//修改指定下标位置的数据
void SeqListAt(SL* ps, size_t pos, SLDataType x)
{
assert(ps);
assert(pos > 0 && pos < ps->size);
assert(ps->size > 0);
ps->a[pos] = x;
}
测试:
#include"SeqList.h"
void test01()
{
SL s1;
SeqListInit(&s1);
SeqListPushBack(&s1, 1);
SeqListPushBack(&s1, 2);
SeqListPushBack(&s1, 3);
SeqListPushBack(&s1, 4);
SeqListPushBack(&s1, 5);
SeqListPopBack(&s1);//删除最后一个元素
SeqListErase(&s1, 2);//删除数组第二个元素
SeqListPushFront(&s1, 20 );
SeqListPrint(&s1);
}
void test02()
{
SL s1;
SeqListInit(&s1);
SeqListPushBack(&s1, 1);
SeqListPushBack(&s1, 2);
SeqListPushBack(&s1, 3);
SeqListPushBack(&s1, 4);
SeqListPushBack(&s1, 5);
SeqListAt(&s1,2,8);
SeqListPrint(&s1);
}
int main()
{
//test01();
test02();
system("pause");
return 0;
}
顺序表缺陷:
1.空间不足,需要扩容。扩容存在一定代价,其次还可能存在一定空间浪费
2.头部或者中部插入删除,需要挪动数据
二、单链表
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑结构是通过链表中的指针链接实现的
实际中链表的结构非常多样,以下情况组合起来有8种链表结构:
(1)单向、双向(是否支持向前访问)
(2)带头、不带头(在链表开头是否有一个不存放数据的头结点)
(3)循环、非循环(是否能通过链表的尾结点直接访问到链表的第一个结点)
单链表头文件:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
typedef int DataType;
//不带头单向非循环链表
typedef struct SListNode//单链表结点结构类型
{
DataType data;//结点数据域
struct SListNode* next;
//结点指针域,指针指向下一个结点,而下一个结点的类型也为SListNode
}SLTNode;
//接口函数
void SListPrint(SLTNode* plist);//显示
void SListPushBack(SLTNode** pplist,DataType x);//尾插
void SListPushFront(SLTNode** pplist, DataType x);//头插
void SListPopBack(SLTNode** pplist);//尾删
void SListPopFront(SLTNode** pplist);//头删
//insert与erase的使用以find为前提,通过find找到插入位置即pos
SLTNode* SListFind(SLTNode* plist, DataType x);//查找
void SListInsert(SLTNode** pplist,SLTNode* pos, DataType x);//在pos位置前插入x
void SListErase(SLTNode** pplist, SLTNode* pos);//删除pos位置的值
单链表源文件:
#include"SingleList.h"
//接口函数实现
void SListPrint(SLTNode* plist)
{
SLTNode* cur = plist;
while (cur!=NULL)//链表的最后一个指针域为NULL
{
printf("%d ",cur->data);//输出结点数据域
cur = cur->next;//通过指针域走向下一个元素
}
printf("\n");
}
void SListPushBack(SLTNode** pplist,DataType x)//这里的pplist是plist的地址
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//开辟一块大小与SLTNode相同的空间
newnode->data = x;//新结点数据域赋值
newnode->next = NULL;//将新结点的指针域置空
if (*pplist==NULL)//*pplist其实就是plist
{
*pplist = newnode;
//如果传入形参为SLTNode*型,实参与形参类型相同,形参的改变不影响实参,形参是实参的一个拷贝
//所以采用指针的方式传递,传入首结点地址,所以采用二级指针SLTNode**
}
else
{
SLTNode* tail = *pplist;//尾结点的指针需要从链表的第一个元素开始查找
while (tail->next!=NULL)
{
tail = tail->next;
}
tail->next = newnode;//尾结点链接新结点
}
}
void SListPushFront(SLTNode** pplist,DataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
newnode->next = *pplist;//插入结点指针域为原来首地址
*pplist = newnode;
}
void SListPopFront(SLTNode** pplist)
{
SLTNode* next= (*pplist)->next;//*与->为同一个优先级,所以加()
free(*pplist);//释放链表首个结点
*pplist = next;
}
void SListPopBack(SLTNode** pplist)
{
//assert(pplist != NULL);
if (*pplist == NULL)//当链表为空
{
return;
}
else if ((*pplist)->next==NULL)//当仅有一个结点
{
free(*pplist);
*pplist = NULL;
}
else
{
SLTNode* tail = *pplist;
SLTNode* prev = NULL;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}//prev为tail的前一个指针,当tail到链表的末尾时,prev指向倒数第二个结点
free(tail);
prev->next = NULL;
}
}
SLTNode* SListFind(SLTNode* plist,DataType x)
{
SLTNode* cur = plist;
while (cur!=NULL)
{
if (cur->data==x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void SListInsert(SLTNode** pplist, SLTNode* pos, DataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
if (pos == *pplist)
{
SListPushFront(pplist,x);
}
else
{
SLTNode* prev = *pplist;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
void SListErase(SLTNode** pplist, SLTNode* pos, DataType x)
{
SLTNode* prev = *pplist;
if (pos == *pplist)
{
SListPopFront(pplist);
}
else
{
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
测试:
#include"SingleList.h"
void test01()
{
SLTNode* plist = NULL;//plist为指向第一个元素的指针
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPushFront(&plist, 0);
SListPopBack(&plist);
SListPopBack(&plist);
SListPopBack(&plist);
SListPopBack(&plist);
SListPrint(plist);
}
void test02()
{
SLTNode* plist = NULL;//plist为指向第一个元素的指针
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SLTNode* pos = SListFind(plist, 3);
if (pos!=NULL)
{
SListInsert(&plist,pos,30);//在数字3前面插入一个30
}
SLTNode* pos1 = SListFind(plist, 1);
if (pos1)
{
SListInsert(&plist, pos1, 10);
}
SListPrint(plist);
}
void test03()
{
SLTNode* plist = NULL;//plist为指向第一个元素的指针
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SLTNode* pos1 = SListFind(plist, 3);
if (pos1)
{
SListErase(&plist, pos1);
}
SLTNode* pos2 = SListFind(plist, 1);
if (pos2)
{
SListErase(&plist, pos2);
}
SListPrint(plist);
}
int main()
{
//test01();
//test02();
test03();
system("pause");
return 0;
}
三、带头双向循环链表
哨兵节点,也是头结点,是一个 dummy node. 可以用来简化边界条件.
是一个附加的链表节点.该节点作为第一个节点,它的值域不存储任何东西.
只是为了操作的方便而引入的.
如果一个链表有哨兵节点的话,那么线性表的第一个元素应该是链表的第二个节点.
双链表头文件:
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
//带头双向循环链表
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;//后继指针域
struct ListNode* prev;//前驱指针域
LTDataType data;
}ListNode;
//接口函数
//void ListInit(ListNode* plist);//初始化
ListNode* ListInit();
void ListDestory(ListNode* plist);//销毁
void ListPushBack(ListNode* plist,LTDataType x);//尾插
void ListPushFront(ListNode* plist, LTDataType x);//头插
void ListPopBack(ListNode* plist);
void ListPopFront(ListNode* plist);
ListNode* ListFind(ListNode* plist,LTDataType x);
void ListInsert(ListNode *pos,LTDataType x);//在pos前插入x
void ListErase(ListNode* pos);//删除pos位置的值
void ListPrint(ListNode* plist);
双链表源文件:
//带头双向循环链表
//哨兵位:
//1.对于一个已经创建和初始化的链表来讲,可以没有数据,即头节点,尾节点等,但是不能没有哨兵位,因为哨兵位并不是帮我们存储数据的,只是来定位链表的。即使是一个空链表,它也会有哨兵位,所以我们可以用哨兵位是否为空来检测程序运行情况。
//2.对于链表来讲,一个很重要的地方就是头的位置,而哨兵位则可以起到定位头的作用,它的下一个节点就是头节点。
//3.在双向循环链表中,如果我们要遍历,那么很难去说明结束条件,而我们可以用哨兵位来作为结束条件,当当前节点为哨兵位时,遍历结束。
ListNode* CreateListNode(LTDataType x)//创建一个新的结点
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
//采用传入参数的模式进行链表的初始化,创造头结点的时候
//考虑到形参与实参的关系,需要传入参数为二级指针
//void ListInit(ListNode** plist)
//{
// *plist = CreateListNode(0);//创建一个新的结点为头节点,数据域为0
// (*plist)->next = *plist;
// (*plist)->prev = *plist;
// //当只有一个头节点存在时,他的前驱结点指向自己,后继域也指向自己,从而实现单独头节点的循环
//}
ListNode* ListInit()
{
//构造一个有哨兵位的空的单链表
ListNode* plist = CreateListNode(0);
plist->next = plist;
plist->prev = plist;
return plist;
}
void ListDestory(ListNode* plist)
{
assert(plist);
ListNode* cur = plist->next;
while (cur!=plist)//逐个释放链表内容
{
ListNode* next = cur->next;
free(cur);
cur = next;
}
free(plist);
}
void ListPushBack(ListNode* plist,LTDataType x)
{
assert(plist);
ListNode* tail = plist->prev;//哨兵位的前驱指针指向尾结点
ListNode* newnode = CreateListNode(x);//创建一个新的存放数据为x的结点
//不论链表为空还是不为空都可以插入
tail->next = newnode;
newnode->prev = tail;
newnode->next = plist;
plist->prev = newnode;
}
void ListPushFront(ListNode* plist,LTDataType x)
{
//plist为哨兵位,始终再链表的最前端
assert(plist);
ListNode* first = plist->next;
ListNode* newcode = CreateListNode(x);
plist->next = newcode;
newcode->prev = plist;
newcode->next = first;
first->prev = newcode;
//plist->newcode->first,其中newcode1为头节点
}
void ListPopFront(ListNode* plist)
{
assert(plist);
assert(plist->next!=plist);
ListNode* first = plist->next;
ListNode* second = first->next;
plist->next = second;
second->prev = plist;
free(first);
}
void ListPopBack(ListNode* plist)
{
assert(plist);
assert(plist->next!=plist);
ListNode* tail = plist->prev;
ListNode* beforetail = tail->prev;//尾结点的前一个
plist->prev = beforetail;
beforetail->next = plist;
free(tail);
tail = NULL;
}
ListNode* ListFind(ListNode* plist,LTDataType x)
{
assert(plist);
ListNode* cur = plist->next;
while (cur!=plist)
{
if (cur->data==x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void ListInsert(ListNode* pos,LTDataType x)
{
assert(pos);
ListNode* newcode = CreateListNode(x);
ListNode* beforepos = pos->prev;
newcode->next = pos;
beforepos->next = newcode;
newcode->prev = beforepos;
pos->prev = newcode;
}
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* beforepos = pos->prev;
ListNode* nextpos = pos->next;
beforepos->next = nextpos;
nextpos->prev = beforepos;
free(pos);
}
void ListPrint(ListNode* plist)
{
ListNode* cur = plist->next;//cur指向头节点
while (cur != plist)//当指向哨兵位时结束打印
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
测试:
#include"list.h"
void test01()
{
//ListNode* plist = NULL;
//ListInit(plist);
ListNode* plist = ListInit();
ListPushBack(plist, 10);
ListPushBack(plist, 9);
ListPushBack(plist, 8);
ListPushBack(plist, 7);
ListPushFront(plist, 6);
ListPushFront(plist, 5);
ListPrint(plist);
ListPopFront(plist);
ListPrint(plist);
ListPopBack(plist);
ListPrint(plist);
ListFind(plist,6);
ListDestory(plist);
}
void test02()
{
ListNode* plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPrint(plist);
ListNode* pos = ListFind(plist, 3);
if (pos)
{
printf("找到了\n");
}
else
{
printf("没找到\n");
}
}
int main()
{
//test01();
test02();
system("pause");
return 0;
};