数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的 数据元素的集合。算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
时间复杂度
//计算算法的调用时间
size_t begin = clock();
size_t end = clock();
时间复杂度:一个算法所花费的时间与其中执行次数成正比例,算法中的基本操作的执行次数为算法的时间复杂度。时间复杂度结果我们使用大O渐进表示法。
大O渐进表示法:用常数1取代运行时间中的所有常数(O(1)代表的就是常数次,所以2N=O(N),之所以常数可以看成1是源于CPU运算的次数很快,这就是底气)。在修改后的运行次数函数中,只保留最高阶项。如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
二分查找起始并不是很常用,因为二分查找的前提是已经排序好了的,而排序是要消耗许多资源的,所以并不常用。二分查找的时间复杂度是O(logN)。
递归调用的时间复杂度计算方法:每次递归调用的执行次数的累加。而且递归调用的过程当中,时间是累计的一去不复返,空间是可以重复利用的。
//计算斐波那契递归的时间复杂度 long long Fib(size_t N) { if (N < 3) return 1; return Fib(N - 1) + Fib(N - 2); }
空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的度量。空间复杂度算的是变量的个数,也是采用大O渐进表示法。空间复杂度主要是通过函数在运行的时候显示申请的额外空间来确定的。
//计算阶乘递归的的时间复杂度
long long Fac(size_t N)
{
if (N == 0)
return 1;
return Fac(N - 1) * N;
}
//每次调用的空间复杂度是常数个,每次调用要建立栈帧。N次递归调用要建立N个栈帧,每个栈帧都有变量,这些变量要累计在一起,因为递归到最深除的时候这些栈帧是同时存在的。递归空间复杂度计算方法和技巧:每次递归调用变量个数的累加。普通调用若用循环写,则不用掉N个栈帧。
线性表
线性表是n个具有相同特征的数据元素的有限序列。线性表逻辑上是线性的,但是物理结构上并不一定是连续的,其在物理上存储时,通常以数组和链式结构的形式存储。常见的线性表:顺序表、链表、栈、队列、字符串。
//数组越界读基本不会被检查出来,但是越界写一般很可能会被检查出来。
int main()
{
int a[10] = { 0 };
//越界读,基本不会被检查出来
printf("%d", a[10]);
printf("%d", a[11]);
//越界写,可能会被检查出来,具体的行为由编译器来定义。vs在2022检查的比较严格。
//a[10] = 0;
//a[11] = 0;
//a[12] = 0;
a[13] = 0;
//a[20] = 0;
return 0;
}
顺序表
顺序表和数组的区别:后者可以不用连续存储,比如开辟了一个10个大小的数组,可以将数据只存储在下标为奇数的位置。而顺序表要求连续存储。
顺序表的静态存储和动态存储
//顺序表的静态存储
#define N 7//N太小了不够用,N太大了造成大量的浪费,所以静态顺序表不太实用
typedef int SLDateType;
typedef struct SeqList
{
SLDateType arr[N];//定长数组
size_t SIZE;//有效数据的个数
}SL;
//顺序表的动态存储
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* arr;//指向堆区动态开辟的数组,空间不够可以直接增容,比较灵活。
int size;//有效数据个数
int capacity;//容量空间的大小
}SL;
SLInit()函数
void SLInit(SL* ps)
{
assert(ps);
ps->arr = NULL;
ps->size = 0;
ps->capacity = 0;
}
SLDestroy()函数
void SLDestroy(SL* ps)
{
assert(ps);
//free(ps->arr);
//ps->capacity = ps->size = 0;
//对上面两行优化如下:
if (ps->arr)//判断arr是否已经为空了
{
free(ps->arr);
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
}
CheckCapacity()函数
void CheckCapacity(SL* ps)
{
assert(ps);
if (ps->capacity == ps->size)//若有效数据的个数和容量对等的话,需要进行扩容处理。
{
int newcapacity = (ps->capacity == 0 ? 4 : ps->capacity * 2);//若是初始化就扩容到4否则扩容2倍
SLDateType* tmp = (SLDateType*)realloc(ps->arr, ps->capacity * sizeof(SLDateType));//realloc如果后面又足够的空间就原地扩容,否则异地扩容,即起始地址要发生改变。当ps->arr是NULL时,相当于malloc。
if (tmp == NULL)//扩容失败返回NULL
{
perror("realloc fail");
exit(-1);//返回0是正常返回,非0才是异常返回,也可以return -1。
}
ps->capacity = newcapacity;//不要忘记修改capacity
ps->arr = tmp;
}
}
SLPushBack()函数
void SLPushBack(SL* ps, SLDateType a)
{
assert(ps);
/*if (ps->capacity == ps->size)
{
ps->capacity = (ps->capacity == 0 ? 4 : ps->capacity * 2);
SLDateType* tmp = (SLDateType*)realloc(ps->arr, ps->capacity * sizeof(SLDateType));
if (tmp == NULL)
{
perror("realloc fail");
return -1;
}
ps->arr = tmp;
}*/
CheckCapacity(ps);
ps->arr[ps->size] = a;
ps->size++;
}
SLPopBack()函数
void SLPopBack(SL* ps)
{
assert(ps);
温柔的检查,无法暴漏问题
//if (ps->size == 0)
//{
// return;
//}
assert(ps->size > 0);//暴力的检查,不用if去判断。尽早暴漏问题,直接报错。
ps->size--;
}
SLInsert()函数
void SLInsert(SL* ps, int pos, SLDateType a)
{
assert(ps);
CheckCapacity(ps);
assert(pos >= 0 && pos <= ps->size);//别忘记了判断pos的合理性
int i = 0;
for (i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = a;
ps->size++;
}
SLErase()函数
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size - 1);//注意这里要减1:pos <= ps->size-1。insert可以在pos->size位置插入,但是erase不可以。
int i = 0;
for (i = pos; i < ps->size - 1; i++)//注意这里是i++,并不是i--,别写错了。
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
SLFind()函数
int SLFind(SL* ps, SLDateType a,int begin)//增加一个参数begin可以提高find的效率
{
assert(ps);
int i;
for (i = begin; i < ps->size; i++)
{
if (ps->arr[i] == a)
{
return i;
}
}
return -1;
}
//由于SLFind新增了一个参数begin,删除链表里的全部值为2的元素再运行时效率是由明显提升的。
pos = SLFind(&sl, 2, 0);
while (pos != -1)
{
SLErase(&sl, pos);
pos = SLFind(&sl, 2, pos);
}
单向链表
链表是一种物理存储结构上非连续、非顺序的存储结构。由于顺序表的缺陷:1、空间不够需要扩容,扩容尤其是异地扩容是有一定代价的,其次还可能存在一定的空间浪费。2、头部或者中部插入删除,需要挪动数据,效率很低。链表对于这些缺陷进行了优化,链表的空间不存在浪费,它是按需申请释放的,而且链表的增删不需要挪动数据。
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
void testslist1()
{
SLTNode* n1 = malloc();//堆区
SLTNode* n2 = malloc();
n1->next = n2;
SLTNode n3;//栈区,这种定义出了作用域就没了,全局变量和静态变量出了作用域还在,但是他们无法销毁。只有动态开辟在堆区的molloc这些函数开辟的空间才可以满足随时开辟与销毁
SLTNode n4;
n3.next = n4;
}
链表可以分为带头和不带头、循环和不循环、单向和双向
BuySLTNode()函数
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newhead = (SLTNode*)malloc(sizeof(SLTNode));
if (newhead == NULL)
{
perror("malloc fail");
return -1;
}
newhead->data = x;
newhead->next = NULL;
return newhead;//别忘记了写返回值
}
CreatSlist()函数
SLTNode* CreatSlist1(int n)
{
SLTNode* newhead = BuySLTNode(n);
SLTNode* phead = newhead;
while (n - 1)
{
newhead->next = BuySLTNode(n-1);
newhead = newhead->next;
n--;
}
return phead;
}
//CreatSlist1是我的实现,运行没问题。CreatSlist是另一种实现方式,大体差不都,只是采用两个变量ptail和phead。
SLTNode* CreatSlist(int n)
{
SLTNode* phead = NULL, *ptail = NULL;
for (int i = 0; i < n; i++)
{
SLTNode* newhead = BuySLTNode(i + 10);
if (phead == NULL)
{
ptail = phead = newhead;
}
else
{
ptail->next = newhead;
ptail = newhead;
}
}
return phead;
}
SLTPushBack()函数
void SLTPushBack(SLTNode** pphead, SLTDataType x)//为什么要穿这个形参SLTNode** pphead也就是phead的指针,因为phead可能为NULL,此时pushback就BuySLTNode功能差不多,并且还要更新phead,所以得传递phead的指针。
{
SLTNode* newhead = BuySLTNode(x);
SLTNode* cur = *pphead;
if (*pphead == NULL)
{
*pphead = newhead;
}
else
{
while (cur->next)
{
cur = cur->next;
}
cur->next = newhead;
}
}
SLTPopBack()函数
void SLTPopBack(SLTNode** pphead)
{
//暴力的检查
assert(*pphead);//不加该行代码的话,该函数只考虑了链表只有一个节点的情况,但是未考虑链表为空的情况,所以加了这么一个暴力的检查防止链表为空。
//温柔的检查,很难让人发现问题,因为它并不是对问题进行报错处理。
//if(*pphead==NULL)
// return;
SLTNode* cur = *pphead;
SLTNode* prv = *pphead;
while (cur->next)
{
prv = cur;
cur = cur->next;
}
if (cur == *pphead)//链表只有一个节点的情况
{
free(*pphead);
*pphead = NULL;
}
else
{
free(cur);
cur == NULL;
prv->next = NULL;
}
}
///
int arr = { 1,2,3,4,5,6 };//arr[i]也是一种解引用,arr是数组第一个元素的地址,aar[i]是对arr+i这个位置进行解引用.
SLTPushFront()函数
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
if (*pphead == NULL)//别把判断写成了赋值,我之前就是这里写错了,调试之后才发现
{
*pphead = BuySLTNode(x);
}
else//其实不用写上面的if情况,因为else的代码也同样适用于if的情况,只是写了上面的if使得代码的可读性更好一些。
{
SLTNode* newhead = BuySLTNode(x);
newhead->next = *pphead;
*pphead = newhead;
}
}
SLTPopFront()函数
void SLTPopFront(SLTNode** pphead)
{
assert(*pphead);
SLTNode* phead = *pphead;
SLTNode* next = phead->next;
if (next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else//其实不用写上面的if情况,因为else的代码也同样适用于if的情况
{
free(*pphead);
*pphead = next;
}
}
SListInser()函数
void SListInsertAfter(SLTNode* pos, SLTDataType x)//改变结构体,有结构体的指针足以,不需要结构体的二级指针。
{
assert(pos);//别忘记了要断言
SLTNode* next = pos->next;
SLTNode* newhead = BuySLTNode(x);
pos->next = newhead;
newhead->next = next;
}
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pos);
if (*pphead == pos)
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* cur = *pphead;
SLTNode* prv = NULL;
while (cur)
{
if (cur == pos)
{
break;
}
prv = cur;
cur = cur->next;
}
prv->next = BuySLTNode(x);
prv->next->next = pos;
}
}
SListErase()函数
void SListEraseAfter(SLTNode* pos)//删除pos的后一个节点好删,若要删除的是pos,需要知道pos的前一个结点
{
assert(pos);
if (pos->next == NULL)//该行x到x+3行我自己写的时候没有考虑到,没有写,后面参考别人的代码补充的。该行代码判断是为了防止pos是链表的最后一个结点。
{
return;
}
SLTNode* next = pos->next->next;
free(pos->next);
pos->next = next;
}
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(*pphead);
if (pos == *pphead)
{
SLTPopFront(*pphead);
return;
}
SLTNode* prv = *pphead;
SLTNode* cur = prv->next;
while (cur != pos)
{
prv = cur;
cur = cur->next;
}
//设该行是x行,x-5到x-1行的代码也可以更改成下面的代码一样
//while (prv->next != pos)
//{
// prv = prv->next;
//}
prv->next = pos->next;
free(pos);//释放了的结点依旧还在,也就是开可以访问,但是内容可能已经被指控了,也就是说之前存在在这个地址上的值已经发生了改变。
}
SLTDestroy()函数
void SLTDestroy(SLTNode** pphead)
{
//assert(*pphead);
//SLTNode* cur = *pphead;
//SLTNode* next = cur->next;
//while (next)
//{
// free(cur);
// cur = next;
// next = next->next;
//}
//free(cur);
//改行是x行,x-1到x-10是我写的,有点小问题,下面是参考代码
assert(*pphead);//其实这边不与要断言,传入的是NULL也可以,下面的代码再*pphead=NULL的时候照样适用
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
SListFind()函数
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
SLTNode* cur = plist;
while (cur)//防止链表为空,如果链表为空即cur=NULL,此时cur->next是对空进行解引用会报错。这里不可以采用cur->next != NULL条件来进行判断,因为i这个判断会漏掉最后一个节点的data。
{
if (cur->data == x)
{
break;
}
else
{
cur = cur->next;
}
}
return cur;
}
双向链表(带哨兵位)
typedef struct ListNode
{
LTDataType val;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
ListInit()函数
LTNode* ListInit()//这个函数的实现并没有想到,next和prev都指向自己
{
LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
BuyListNode()函数
LTNode* BuyListNode(LTDataType x)
{
LTNode* newhead = (LTNode*)malloc(sizeof(LTNode));
if (newhead == NULL)
{
perror("malloc fail");
return -1;
}
newhead->next = newhead->prev = NULL;
newhead->val = x;
return newhead;
}
LTFind()函数
LTNode* LTFind(LTNode* phead, LTDataType x)
{
//assert(phead->next != phead);这句话是不需要的,我们遇到空链表直接返回NULL
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->val == x)
{
return cur;
}
else
cur = cur->next;
}
return NULL;
}
LTEmpty()函数
bool LTEmpty(LTNode* phead)
{
return phead->next == phead;
}
LTSize()函数
size_t LTSize(LTNode* phead)
{
int count = 0;
LTNode* cur = phead;
while (cur->next != phead)
{
count++;
cur = cur->next;
}
return count;
}
LTDestroy()函数
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* tmp = cur->next;
LTErase(cur);
cur = tmp;
}
free(phead);//本行并没有考虑到
}
顺序表和链表区别
顺序表物理上连续,支持随机访问,其访问的复杂度是O(N)。优点是尾插尾删效率高,下标的随机访快。缺点是空间不够需要扩容(扩容代价大),头部或者中间的插入删除效率低需要挪动数据。顺序表缓存利用率高,常应用于元素高效存储以及频繁访问的场景。
链表逻辑上是连续的但是物理上不一定是连续的,不支持随意访问,其访问的复杂度是O(N)。链表的有点事不需要扩容,按需申请释放小块结点的内存,任意位置插入删除的效率都很高。缺点是缓存利用率不高,不支持随机访问。
栈
进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈的实现一般可以使用数组或者链表实现,数组的结构实现相对更优一些,因为数组在尾部插入删除数据的代价比较小。
// 下面是定长的静态栈的结构,实际中一般不实用,所以我们主要实现下面的支持动态增长的栈
#define N 10
typedef struct Stack
{
STDataType _a[N];
int _top; // 栈顶
}Stack;
// 支持动态增长的栈
typedef struct Stack//Stack的结构我是没有想到的,top特别难想到
{
STDataType* a;
int capacity;//容量
int top;//栈顶
}ST;
StackInit()函数
void StackInit(ST* ps)
{
//ps->a = NULL;
//ps->capacity = 0;
//ps->top = 0;//初始为0,表示栈顶位置下一个位置的下标
//上面三行初始化的方法并没有开空间,后期处理起来不是很方便,并不支持,但是没有错。
ps->a = (STDataType*)malloc(sizeof(STDataType) * 4);
if (ps->a == NULL)
{
perror("malloc fail");
return -1;
}
ps->capacity = 4;
ps->top = 0;//top初始为0,表示指向栈顶元素的下一个位置。所以stackpush是先放数据在对top进行++
}
StackDestroy()函数
void StackDestroy(ST* ps)
{
assert(ps);//该行代码没有写
free(ps->a);
ps->a = NULL;//该行代码没有写
ps->capacity = 0;
ps->top = 0;
}
StackPush()函数
void StackPush(ST* ps, STDataType x)
{
//扩容
if (ps->capacity == ps->top)
{
STDataType* tmp = (STDataType*)realloc(ps->a, sizeof(STDataType) * ps->capacity * 2);//扩容的写法需要掌握
//上行代码请不要直接赋值给ps->a,以防扩容失败
if (ps->a == NULL)
{
perror("relloc fail");
return -1;
}
ps->a = tmp;
ps->capacity = ps->capacity * 2;
}
ps->a[ps->top] = x;
ps->top++;
}
StackPop()函数
void StackPop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));//防止空栈还继续pop
ps->top--;//此处不用free,因为malloc的是一整块空间,要释放一块释放,不存在free一部分空间的说法。
}
StackEmpty()函数
bool StackEmpty(ST* ps)
{
assert(ps);
return (ps->top == 0);//还有一种写法return !ps->top,这也说明布尔值可以用整型值来表达,0为假非0外真
}
队列(先进先出)
对于栈的实现,数组小优于链表。但是对于队列的实现,链表远远优于数组,在队列中数组的效率太差劲了。队列进行插入操作的一端称为对尾,进行删除操作的一端称为队头。
//队列的结点结构
typedef struct QueueNode
{
QDataType x;
struct QueueNode* next;
}Qnode;
//对列的结构
typedef struct Queue//队列的结构的构成我没有想到,特别是还要一个ptail和size
{
Qnode* phead;
Qnode* ptail;
int size;
}Queue;
QueueInit()函数
void QueueInit(Queue* q)//我最开始考虑初始化的时候是打算让q->phead=q->ptail=malloc一个节点的空间的,但这不对,因为有了节点之后,就相当于已经存入了数据,但是初始化队列是没有任何数据的
{
assert(q);
q->phead = NULL;
q->ptail = NULL;
q->size = 0;
}
QueueDestroy()函数
void QueueDestroy(Queue* q)
{
assert(q);
Qnode* cur = q->phead;
while (cur)
{
Qnode* next = cur->next;
free(cur);//cur在这里free之后可以不用置空,因为出了这个函数就无人可以放为cur这个地址了,但是phead和ptail要置空,因为出了函数依旧可以访问通过phead和ptail访问这两个已经不归属于我们的地址。
cur = next;
}
q->phead = q->ptail = NULL;//该行代码我没有考虑到,确实要置空,不然可以通过q->phead和q->ptail访问到空间,但是空间的权限已经不输入我们了。
q->size = 0;
}
//关于free之后是否置空的问题,也看这个野指针是否会被访问到,如果free之后没有人再找的到这个地址,也就是找不到存储该地址的变量,我认为不知空也没有问题。
QueuePush()函数
void QueuePush(Queue* q, QDataType x)
{
assert(q);
//生成新结点
Qnode* newnode = (Qnode*)malloc(sizeof(Qnode));
if (newnode == NULL)
{
perror("malloc fail");
return -1;
}
newnode->x = x;
newnode->next = NULL;
//将新结点链入链表当中
if (q->phead == NULL)
{
assert(q->ptail == NULL);
q->phead = q->ptail = newnode;
}
else
{
q->ptail->next = newnode;
q->ptail = newnode;
}
q->size++;
}
QueuePop()函数
void QueuePop(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));//非空才能pop
Qnode* next = q->phead->next;
free(q->phead);
q->phead = next;
if (q->phead == NULL)//说明队列中只有一个结点,pop之后phead和ptail都要置空。
{
q->ptail = NULL;
}
q->size--;//该行代码我未写
}
QueueEmpty()函数
bool QueueEmpty(Queue* q)
{
assert(q);
return q->phead == NULL && q->ptail == NULL;//我的写法是return q->phead == NULL,
}
int QueueSize(Queue* q)
{
assert(q);
/*int size = 0;
QNode* cur = pq->head;
while (cur)
{
cur = cur->next;
++size;
}
return size;*///这段备注释的代码,采用这种方法时间复杂度是O(N),效率低。于是我们想到定义一个全局变量size,但是当有连个结构体QNode* q1和QNode* q2时,会有问题。两个结构体各push一个数据,size会加2。于是我们将size封装到Queue结构体中。
return q->size;
}