数据结构是计算机存储、组织数据的方式。顺序表底层就是数组。顺序表(物理结构和逻辑结构都连续)是线性表(具有相同特性的数据结构的集合;物理结构不一定连续;逻辑结构一定连续)的一种。
常见的线性表:顺序表、链表、栈、队列、字符串..... 线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
一、
···数组的分类:
int arr[10]={0};定长的数组
int* arr;动态内存开辟,确定大小之后再去动态申请
···顺序表分类:
¥静态顺序表的定义:(底层是定长数组,已经确定大小)
坏处:数组大小给小了导致空间不够用,数组大小给打了导致空间浪费。
一般将所开辟的数组大小设置为宏定义,方便后续的改动。 #define N 100
¥动态顺序表的定义:
可以进行动态增容,可以更好的把控空间大小。
我们不知道顺序表中存储的数据类型是什么,所以给数据类型重定义typedef
注意结构体重命名的写法
二、单链表各种操作
//顺序表不为空有两种情况:(需要特别注意这种情况的是当需要尾删的时候)
//1.传参传的地址是NULL
//2.顺序表有地址即顺序表存在,但是其size为0,当访问下标的时候要size-1,此时就完蛋了
//capacity/size ++/--
//在指定位置pos之前插入数据
//那么pos>=0&&pos<=size
//最之前可以在以一个数据之前插入即第0个位置
//最之后即可以在最后一个数据的下一个位置之前插入,即实现尾插
//这个断言同时也可以拦截顺序表为空的第二种情况
//对通讯录操作实际上就是对顺序表进行操作,只是把结构体重命名作为顺序表中一个空间的ElemType了
//顺序表头文件要用到通讯录头文件的struct personInfo,而Contact.h要用到SeqList.h的SL,不能互相include
//不能互相包含头文件,交叉使用头文件会导致程序报错
//这里要用到前置声明
//可以写成typedef struct SeqList Contact;//这一步就叫做前置声明
//不可以写成typedef SL Contact;因为在Contact.h文件中没有包含SeqList.h,只能使用最原始的名字;如果包含了就能使用包含的那个头文件中typedef之后的名字了
//在SeqList.h文件中虽然使用了typedef进行了重命名,但是是先定义然后改的名字
//在Contact.c文件中肯定会用到Contact.h中的内容,这个肯定要包含,同时,因为对通讯录操作就是对顺序表进行操作,可以节省一些步骤,所以也要包含SeqList.h文件
//使用o(1)复杂度的程序,删除数组中所有的等于value的值
//也就是说不能再开辟一块空间
//双指针法:创建两个指针变量src、dst同时指向起始位置
// 若src指向的值是value,则src++
// 若src指向的值不是value,则nums[dst]=nums[src],src++,dst++
// 最后src指向结尾,dst移动的次数(dst指向的坐标位置)就是当前数组中还剩多少个数据
int removeElement(int* nums,int numsSize,int val)
{
int src,dst;
src=0,dst=0;
while(src<numSize)
{
if(nums[src]==val)
{
src++;
}
else
{
nums[dst]=nums[src];
src++;
dst++;
}
}
return dst;//返回新数组的有效长度
}
//将nums1和nums2两个非递减的有序数组合并到nums1这一个数组中
//方法一:
//将nums2中的数据先都放在nums1的后面,然后对nums1进行排序
//方法二:
//三个指针,两个分别指向两个数组的最后一个有效元素,另一个指针指向合并后nums1的最后一个位置
//两个指针处的数据比较大小,谁大谁往第三个指针那里放,然后指针减减,当有指针为0时结束循环
//同时注意:不可能两个指针同时为0,因为一次只挪动一个数据,必定有一个先移完
//结束循环之后,如果nums1中的那个指针不为0不用管,因为nums1本身有序
//如果nums2中的指针不为0,需要将他放到nums1中的第三个指针里
void merge(int* nums1,int m,int* nums2,int n)//m和n分别为nums1和nums2的初始有效个数
{
int l1=m-1;
int l2=n-1;
int l3=m+n-1;
while(l1>=0&&l2>=0)
{
if(nums1[l1]>nums2[l2])
{
nums1[l3--]=nums1[l1--];
}
else
{
nums1[l3--]=nums2[l2--];
}
}
while(l2>=0)
{
nums1[l3]=nums2[l2--];
}
}
//在单链表中插入一个节点时,需要注意如果链表是空链表,那么链表明就是空指针,而空指针不能解引用
//即SLTNode* ptail=phead(被传参的链表);//这样是会报错的
//所以当为空链表的时候,应该让ptail直接指向新的节点
//如果不是空链表就循环找要插入那个点,然后改变指针指向
SLTNode* plist=NULL;
SLTPushBack(plist,1);
void SLTPushBack(SLTNode* phead,SLTDataType x);
//形参的改变要影响实参的话必须要传地址
//这样传参会导致只有在被调用的函数中改变了,除了被调函数的作用域就再恢复原来的状态
//因为要改变的是plist的内容,虽然plist存储的内容是指针,但是只有取地址才能实现地址传参
//才能真正改变plist的内容,即才能实现改变plist指向的空间
//要写成
SLTNode* plist=NULL;
SLTPushBack(&plist,1);//对一级指针取地址就要用二级指针来接收
void SLTPushBack(SLTNode** pphead,SLTDataType x);
//该处pphead不能为NULL,因为后面要对他进行解引用来用指针指向的节点来找位置
//而*pphead可以为空,此处代表pphead的内容为空,即pphead指向NULL
//*plist(解引用) 第一个节点 =**pphead(对*pphead解引用)
//plist 指向第一个节点的指针 =*pphead(对pphead解引用)
//&plist 指向第一个节点的指针的地址 =pphead(形参与实参相等)
//删除节点时,pphead和*pphead都不能为空,因为空指针不能解引用和为空删不了
//链表尾删
void SLTPopBack(SLTNode** pphead)
{
//链表不为空
assert(pphead&&*pphead);
//链表只有一个节点
if((*pphead)->next==NULL)//->的优先级高于*
{
free(*pphead);
*pphead=NULL;
}
//链表有多个节点
else
{
SLTNode* prev=*pphead;
SLTNode* ptail=*pphead;
while(ptail->next)
{
prev=ptail;
ptail=ptail->next;
}
free(ptail);
ptail=NULL;
prev->next=NULL;//当只有一个结点的时候,这一个节点被释放了,就不能再用next来指向下一个了,会非法访问
}
}
//链表头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead&&*pphead);
//只有一个结点的时候也行,因为那一个结点的下一个是NULL
SLTNode* next=(*pphead)->next;
free(*pphead);
*pphead=next;
}
//当指向链表头部的指针可能发生变化时,传参的链表名就需要使用二级指针
//在指定位置之前插入数据
//先找到那个数据,即找到指向某个节点的指针
void SLTInsert(SLTNode** pphead,SLTNode* pos,SLTDataType x)
{
assert(pphead&&*pphead);
assert(pos);
//空指针不可以解引用
//当链表为空时,对应的pos也为空,那么就找不到到底在哪个位置之前插入数据
SLTNode* newnode=SLTBuyNode(x);//开辟空间
if(pos==pphead)
{
SLTPushFront(pphead,x);//调用头插函数
}
else
{
SLTNode* prev=*pphead;
while(prev->next!=pos)//这个while循环不能规避头插的情况(头插:最后会出现空指针解引用的情况)
{
prev=prev->next;
}
prev->next=newnode;
newnode->next=pos;
}
}
//最后一个节点的指针指向NULL,所以在最后一个节点的后面插入时,不会出现问题
//删除某个节点之后不要忘了把指向这个节点的指针置为空
free(pos);
pos=NULL;
//删除某个位置pos之后的节点
pos->next=pos->next->next;
free(pos->next);
pos->next=NULL;
//这是错误的,把pos->next释放掉了,就相当于把pos->next->next释放掉了,导致pos为野指针
//正确做法:把初始的pos->next存储下来
//删除倒数第二个位置的下一个即删除最后一个,也可以实现,因为最后一个节点指向NULL
SLTNode* del=pos->next;
pos->next=del->next;
free(del);
del=NULL;
//链表的销毁
void SListDestroy(SLTNode** pphead)//因为要改变头节点的指向
{
assert(pphead&&*pphead);
SLTNode* pcur=*pphead;
while(pcur)
{
SLTNode* next=pcur->next;
free(pcur);
pcur=next;
}
*pphead=NULL;
}
//顺序表不为空有两种情况:(需要特别注意这种情况的是当需要尾删的时候)
//1.传参传的地址是NULL
//2.顺序表有地址即顺序表存在,但是其size为0,当访问下标的时候要size-1,此时就完蛋了
//capacity/size ++/--
//在指定位置pos之前插入数据
//那么pos>=0&&pos<=size
//最之前可以在以一个数据之前插入即第0个位置
//最之后即可以在最后一个数据的下一个位置之前插入,即实现尾插
//这个断言同时也可以拦截顺序表为空的第二种情况
//对通讯录操作实际上就是对顺序表进行操作,只是把结构体重命名作为顺序表中一个空间的ElemType了
//顺序表头文件要用到通讯录头文件的struct personInfo,而Contact.h要用到SeqList.h的SL,不能互相include
//不能互相包含头文件,交叉使用头文件会导致程序报错
//这里要用到前置声明
//可以写成typedef struct SeqList Contact;//这一步就叫做前置声明
//不可以写成typedef SL Contact;因为在Contact.h文件中没有包含SeqList.h,只能使用最原始的名字;如果包含了就能使用包含的那个头文件中typedef之后的名字了
//在SeqList.h文件中虽然使用了typedef进行了重命名,但是是先定义然后改的名字
//在Contact.c文件中肯定会用到Contact.h中的内容,这个肯定要包含,同时,因为对通讯录操作就是对顺序表进行操作,可以节省一些步骤,所以也要包含SeqList.h文件
//使用o(1)复杂度的程序,删除数组中所有的等于value的值
//也就是说不能再开辟一块空间
//双指针法:创建两个指针变量src、dst同时指向起始位置
// 若src指向的值是value,则src++
// 若src指向的值不是value,则nums[dst]=nums[src],src++,dst++
// 最后src指向结尾,dst移动的次数(dst指向的坐标位置)就是当前数组中还剩多少个数据
int removeElement(int* nums,int numsSize,int val)
{
int src,dst;
src=0,dst=0;
while(src<numSize)
{
if(nums[src]==val)
{
src++;
}
else
{
nums[dst]=nums[src];
src++;
dst++;
}
}
return dst;//返回新数组的有效长度
}
//将nums1和nums2两个非递减的有序数组合并到nums1这一个数组中
//方法一:
//将nums2中的数据先都放在nums1的后面,然后对nums1进行排序
//方法二:
//三个指针,两个分别指向两个数组的最后一个有效元素,另一个指针指向合并后nums1的最后一个位置
//两个指针处的数据比较大小,谁大谁往第三个指针那里放,然后指针减减,当有指针为0时结束循环
//同时注意:不可能两个指针同时为0,因为一次只挪动一个数据,必定有一个先移完
//结束循环之后,如果nums1中的那个指针不为0不用管,因为nums1本身有序
//如果nums2中的指针不为0,需要将他放到nums1中的第三个指针里
void merge(int* nums1,int m,int* nums2,int n)//m和n分别为nums1和nums2的初始有效个数
{
int l1=m-1;
int l2=n-1;
int l3=m+n-1;
while(l1>=0&&l2>=0)
{
if(nums1[l1]>nums2[l2])
{
nums1[l3--]=nums1[l1--];
}
else
{
nums1[l3--]=nums2[l2--];
}
}
while(l2>=0)
{
nums1[l3]=nums2[l2--];
}
}
//在单链表中插入一个节点时,需要注意如果链表是空链表,那么链表明就是空指针,而空指针不能解引用
//即SLTNode* ptail=phead(被传参的链表);//这样是会报错的
//所以当为空链表的时候,应该让ptail直接指向新的节点
//如果不是空链表就循环找要插入那个点,然后改变指针指向
SLTNode* plist=NULL;
SLTPushBack(plist,1);
void SLTPushBack(SLTNode* phead,SLTDataType x);
//形参的改变要影响实参的话必须要传地址
//这样传参会导致只有在被调用的函数中改变了,除了被调函数的作用域就再恢复原来的状态
//因为要改变的是plist的内容,虽然plist存储的内容是指针,但是只有取地址才能实现地址传参
//才能真正改变plist的内容,即才能实现改变plist指向的空间
//要写成
SLTNode* plist=NULL;
SLTPushBack(&plist,1);//对一级指针取地址就要用二级指针来接收
void SLTPushBack(SLTNode** pphead,SLTDataType x);
//该处pphead不能为NULL,因为后面要对他进行解引用来用指针指向的节点来找位置
//而*pphead可以为空,此处代表pphead的内容为空,即pphead指向NULL
//*plist(解引用) 第一个节点 =**pphead(对*pphead解引用)
//plist 指向第一个节点的指针 =*pphead(对pphead解引用)
//&plist 指向第一个节点的指针的地址 =pphead(形参与实参相等)
//删除节点时,pphead和*pphead都不能为空,因为空指针不能解引用和为空删不了
//链表尾删
void SLTPopBack(SLTNode** pphead)
{
//链表不为空
assert(pphead&&*pphead);
//链表只有一个节点
if((*pphead)->next==NULL)//->的优先级高于*
{
free(*pphead);
*pphead=NULL;
}
//链表有多个节点
else
{
SLTNode* prev=*pphead;
SLTNode* ptail=*pphead;
while(ptail->next)
{
prev=ptail;
ptail=ptail->next;
}
free(ptail);
ptail=NULL;
prev->next=NULL;//当只有一个结点的时候,这一个节点被释放了,就不能再用next来指向下一个了,会非法访问
}
}
//链表头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead&&*pphead);
//只有一个结点的时候也行,因为那一个结点的下一个是NULL
SLTNode* next=(*pphead)->next;
free(*pphead);
*pphead=next;
}
//当指向链表头部的指针可能发生变化时,传参的链表名就需要使用二级指针
//在指定位置之前插入数据
//先找到那个数据,即找到指向某个节点的指针
void SLTInsert(SLTNode** pphead,SLTNode* pos,SLTDataType x)
{
assert(pphead&&*pphead);
assert(pos);
//空指针不可以解引用
//当链表为空时,对应的pos也为空,那么就找不到到底在哪个位置之前插入数据
SLTNode* newnode=SLTBuyNode(x);//开辟空间
if(pos==pphead)
{
SLTPushFront(pphead,x);//调用头插函数
}
else
{
SLTNode* prev=*pphead;
while(prev->next!=pos)//这个while循环不能规避头插的情况(头插:最后会出现空指针解引用的情况)
{
prev=prev->next;
}
prev->next=newnode;
newnode->next=pos;
}
}
//最后一个节点的指针指向NULL,所以在最后一个节点的后面插入时,不会出现问题
//删除某个节点之后不要忘了把指向这个节点的指针置为空
free(pos);
pos=NULL;
//删除某个位置pos之后的节点
pos->next=pos->next->next;
free(pos->next);
pos->next=NULL;
//这是错误的,把pos->next释放掉了,就相当于把pos->next->next释放掉了,导致pos为野指针
//正确做法:把初始的pos->next存储下来
//删除倒数第二个位置的下一个即删除最后一个,也可以实现,因为最后一个节点指向NULL
SLTNode* del=pos->next;
pos->next=del->next;
free(del);
del=NULL;
//链表的销毁
void SListDestroy(SLTNode** pphead)//因为要改变头节点的指向
{
assert(pphead&&*pphead);
SLTNode* pcur=*pphead;
while(pcur)
{
SLTNode* next=pcur->next;
free(pcur);
pcur=next;
}
*pphead=NULL;
}
三、双向链表(双向有节点循环链表)
//初始化 传参传一级指针
//尾插 最后两行不能交换顺序
//注意初始指向哨兵位的下一个位置和while的循环条件
//头插 两行代码顺序不能变
//