早在我们用C语言写出“hello world!”这一程序之前,就听到老师说,程序=数据结构+算法,那么什么是数据结构呢?下面我们便来了解一下:
数据结构顾名思义就是数据在计算机中的存储结构,当然数据结构有很多种(线性结构,树状结构,网状结构),今天我们只介绍最基础的两种线性结构——顺序表和链表
1.顺序表
1.1顺序表和数组的关系
- 顺序表的底层结构其实就是数组,我们对数组进行封装,实现常用的增删查改等接口便是顺序表
1.2顺序表分类
1.2.1 静态顺序表:使用定长数组存储元素,如下
静态顺序表的缺点显而易见:空间给少了不能用,给大了浪费,用的不多,我们就先不实现
1.2.2动态顺序表
动态顺序表是我们以后比较常用的,所以我们接下来便来实现一下
1.3动态顺序表的实现
下面是我们要实现的几个常用接口:
typedef int SLDataType;
typedef struct SeqList {
int* arr;
int size;//有效数据个数
int capacity;//空间容量
}SL;
//初始化和销毁
void SLInit(SL* ps);
void SLDestroy(SL* ps);
//打印
void SLPrint(SL* ps);
//扩容
void SLCheckCapacity(SL* ps);
//头部插入删除 / 尾部插入删除
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
//指定位置之前插入/删除数据
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);
int SLFind(SL* ps, SLDataType x);
1.3.1动态顺序表的初始化、销毁和打印
这部分内容比较简单直接附上代码
//初始化
void SLInit(SL* ps){
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
//销毁
void SLDestroy(SL* ps){
assert(ps);
if(ps->arr){
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
//打印
void SLPrint(SL* ps){
assert(ps);
for (int i = 0; i < ps->size; ++i) {
printf("%d ",ps->arr[i]);
}
printf("\n");
}
动态顺序表初始化的时候可以申请空间,也可以不申请空间,我在这里没有申请
1.3.2检查内存是否够用,不够用就扩容
void SLCheckcapCity(SL * ps)
{
if (ps->capacity == ps->size){
SLDotaType* ptr;
//在这里用三目操作符来防止传过来的顺序表的内存空间为零
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
ptr = (SLDotaType*)realloc(ps->arr, newcapacity * sizeof(SLDotaType));
if (ptr == NULL){
perror(realloc);
exit(1);
}
ps->arr = ptr;
ps->capacity = newcapacity;
}
}
1.3.4尾部和头部的 插入\删除
尾插和尾删较为简单,尾插就直接在顺序表后面插入数据再令顺序表的size++就好,尾删直接令size--就好了,我们直接看代码
void SLPushback(SL* ps, SLDotaType x){
//先断言
assert(ps);
SLCheckcapCity(ps);
ps->arr[ps->size++]=x;
}
void SLPopback(SL* ps){
assert(ps);
assert(ps->size);
--ps->size;
}
而头插和头删则需要我们将顺序表中的数据向后或者向前移,如图我们想在1的前面插入一个6,我们则需要将1~5这五个元素向后移一个单位
类比一下我们想要删除1则需要2~5这四个元素向右移一个单位 代码如下:
void SLPushFront(SL*ps,SLDotaType x){
assert(ps);
SLCheckcapCity(ps);
for (int i=ps->size;i>0;i--){
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
ps->size++;
}
void SLPopFront(SL* ps){
assert(ps);
assert(ps->size);
for (int i = 0; i < ps->size-1; i++) {
ps->arr[i] = ps->arr[i + 1];
}
--ps->size;
}
1.3.5指定位置之前的插入和删除
我们把前面介绍的头插头删理解了这部分其实就没什么问题了,其实就相当于我们把指定位置当作顺序表的“头”,对它进行头插头删就行了
void SLInsert(SL* ps, int pos, SLDataType x){
assert(ps);
assert(pos <= ps->size && pos >= 0);
SLCheckCapacity(ps);
for (int i = ps->size; i > pos; --i) {
ps->arr[i] = ps->arr[i-1];
}
ps->arr[pos] = x;
++ps->size;
}
void SLErase(SL* ps, int pos){
assert(ps);
assert(pos < ps->size && pos >= 0);
for (int i = pos; i < ps->size-1; ++i) {
ps->arr[i] = ps->arr[i+1];
}
--ps->size;
}
int SLFind(SL* ps, SLDataType x){
assert(ps);
for (int i = 0; i < ps->size; ++i) {
if(ps->arr[i] == x ){
return i;
}
}
return -1;
}
2.链表
2.1什么是链表?
链表就像是一列火车,车厢里存数据,车厢和车厢之间用链子相连。这里的车厢就是链表的结点,那么链子是什么呢?当然是指针啦!那么我们链表节点的就可以这样声明:
那么链表的物理结构应该是这个样子的(尾节点的next要指向NULL否则就会成为野指针)
当然这只是单向不循环链表下面我们便来看看链表的分类
2.2链表的分类
链表有三种结构可选分别是:带头和不带头、单向还是双向、循环还是不循环,两两组合可以形成八种链表如下图
我们在网上刷题遇到的链表为往往是单向不带头不循环链表,简称单链表,那么我们今天就来实现一下单链表的基础功能
2.3链表的实现
常用接口和顺序表差不多,不过值得注意的是,和顺序表不同,我们的链表的常用功能大都是通过修改指针的指向来完成的,所以我们传参时的实参要是指针的地址,用来接收实参的形参便要设计成二级指针,由于链表是一个一个节点串起来的,所以我们不必初始化,也不必担心内存问题
具体如下:
typedef int SLTDataType;
typedef struct SListNode{
SLTDataType val;
struct SListNode* next;
}SLTNode;
void SLTPrint(SLTNode* phead);
//头部插入删除/尾部插入删除
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//销毁链表
void SListDesTroy(SLTNode** pphead);
2.3.1尾部和头部的 插入\删除
开始之前我们先来实现一个开辟新节点的函数:
//节点创建
SLTNode* BuyNode(SLTDataType x){
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode==NULL){
perror(malloc);
exit(1);
}
newnode->Data = x;
newnode->next = NULL;
return newnode;
}
头插和头删较为简单,我们只需要创建一个新节点让新节点的next指针指向头节点,再让*pphead指向新节点就好了,类推一下头删的方法就是新建一个指针prve来记录头节点的next的位置,随后释放掉头节点,再让指向头节点的指针指向prve,如下:
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x){
assert(pphead);
SLTNode* newnode = (SLTNode*)BuyNode(x);
if (*pphead == NULL){
*pphead = newnode;
}
else{
newnode->next = *pphead;
*pphead = newnode;
}
}
//头删
void SLTPopFront(SLTNode** pphead){
assert(pphead && *pphead);
SLTNode* prve = (*pphead)->next;
free(*pphead);
*pphead = prve;
}
尾部的插入和删除则需要先找到链表的尾巴在哪里,然后尾插直接让尾节点的next指向新节点就好了,尾删则需要用一个指针来记录尾节点的前一个节点也就是新的尾节点,释放掉尾节点后要将新的尾节点的next置空,否则会出现野指针,代码如下:
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x){
assert(pphead);
SLTNode* newnode = BuyNode(x);
if (*pphead == NULL){
*pphead = newnode;
}
else{
//找尾
SLTNode* pcur = *pphead;
while (pcur->next){
pcur = pcur->next;
}
pcur->next = newnode;
}
}
//尾删
void SLTPopBack(SLTNode** pphead){
assert(pphead&&*pphead);
if ((*pphead)->next == NULL){
free(*pphead);
*pphead = NULL;
}
else {
SLTNode* prve = *pphead, * ptail = *pphead;
while (ptail->next){
prve = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
prve->next = NULL;
}
}
2.3.2查找元素和指定位置之前和之后的插入
查找元素:
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x){
SLTNode* pcur = phead;
while (pcur){
if (pcur->Data == x){
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
- 指定位置之前的插入
要实现在指定位置之前插入数据,我们便需要找到指定位置的前一个节点,新建一个指针prve记录该节点的next指向的位置,让后让该节点的next指向新节点,再让新节点的next赋值为prve。如下:
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead);
assert(pos);
if (*pphead == pos) {
SLTPushFront(pphead, x);
}
else{
SLTNode* newnode = (SLTNode*)BuyNode(x);
SLTNode* prve = *pphead;
while (prve->next != pos){
prve = prve->next;
}
newnode->next = pos;
prve->next = newnode;
}
}
- 指定位置之后的插入
指定位置之后插入数据就简单了,我们秩序让新节点的next和该节点的next相等,再让该节点的next指向新节点就好了,如下
void SLTInsertAfter(SLTNode* pos, SLTDataType x){
assert(pos);
SLTNode* newnode = (SLTNode*)BuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
2.3.3删除指定位置的节点&删除指定位置之后的节点
和插入差不多,指定位置节点的删除也要找到指定位置之前一个节点,让之前的节点指向之后的节点,将该节点的next指针置空后将该节点free掉就完成了,删除指定位置之后的节点就没那么麻烦,我们只需要新建一个指针prve指向该节点往后两个节点,随后将该节点下一个节点的next置空,随后将该节点的next free掉,然后将prve赋给该节点的next完成了
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);
SLTNode* plist = *pphead;
if (plist == pos){
plist = (*pphead)->next;
*pphead = plist;
}
else{
while (plist->next != pos){
plist = plist->next;
}
plist->next = pos->next;
}
free(pos);
pos = NULL;
}
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
if (pos->next == NULL){
return;
}
else{
SLTNode* prve = pos->next->next;
free(pos->next);
pos->next = prve;
}
}
2.3.4链表的销毁
这个没啥好说的,while循环遍历删除每一个节点就好了
void SListDesTroy(SLTNode** pphead)
{
assert(pphead&&*pphead);
SLTNode* prve = *pphead;
SLTNode* pcur = *pphead;
while (prve->next!=NULL){
prve = prve->next;
free(pcur);
pcur = prve;
}
*pphead = NULL;
}
好的!今天的学习就到此为止吧!