1.基础知识点
数据结构:在内存中存储内容的结构(实现增删查改)
算法:解决某些问题的策略
二者相互影响
判断一个程序的效率需要用复杂度
时间复杂度:一个函数,它定量描述了该算法的运行时间。
空间复杂度:一个函数,它定量描述了该算法的开辟的空间大小。
大O渐进法:指假设该程序的运行向无穷逼近,时间上运行的最高量级作为时间复杂度的描述,空间上产生了多少变量空间的最高量级作为空间复杂度的描述
2.线性表
线性表:结构为遵循一个头一个尾,每个元素首尾相连的排列形式
顺序表:存储的内容地址是连续排列的
分静态与动态——>动态区别于静态为内存可以申请扩容
动态扩容的原则:扩容为原先内存大小的2倍,但是不减容
原因:扩容2倍比较合适,扩容太大有太多空间剩余未利用,扩容小了又要频繁的扩容空间导致程序的效率变慢
//初始化
void SeqListInit(SeqList* ps)
{
ps->a = NULL;
ps->size = ps->capacity = 0;
}
//销毁
void SeqListDestroy(SeqList* ps)
{
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
//增容
void checkcapacity(SeqList* ps)
{
if (ps->capacity == ps->size)
{
int Newcapacity = (ps->capacity == 0) ? 4 : (2 * ps->capacity);
//为什么是两倍,因为两倍比较合适,太大浪费空间,太小浪费时间
SLDateType* ret = (SLDateType*)realloc(ps->a, Newcapacity * sizeof(SLDateType));
if (ret == NULL)
{
perror("realloc");
return 1;
}
ps->a = ret;
ret = NULL;
ps->capacity = Newcapacity;
}
}
//尾增
void SLpushback(SeqList* ps, SLDateType x)
{
checkcapacity(ps);
ps->a[ps->size] = x;
ps->size++;
}
//头增
void SLpushFront(SeqList* ps, SLDateType x)
{
checkcapacity(ps);
for (size_t i = 0; i < ps->size; i++)
{
ps->a[ps->size - i] = ps->a[ps->size - i - 1];
}
ps->a[0] = x;
ps->size++;
}
//打印
void PrintSL(SeqList* ps)
{
for (size_t i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
//尾删
void SLpopback(SeqList* ps)
{
assert(ps);
//1.报错的检查
//assert(ps->size > 0);
//
//2.不报错,直接忽略这次操作
if (ps->size == 0)
{
return;
}
ps->size--;
}
//头删
void SLpopFront(SeqList* ps)
{
assert(ps);
if (ps->size == 0)
{
return;
}
for (size_t i = 0; i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
// 顺序表查找
int SLfind(SeqList* ps, SLDateType x)
{
assert(ps);
for (size_t i = 0; i < ps->size; i++)
{
if (x == ps->a[i])
{
return i;
}
}
return -1;
}
// 顺序表在pos位置插入x
void SLpushpos(SeqList* ps, size_t pos, SLDateType x)
{
checkcapacity(ps);
for (size_t i = ps->size; i > pos; i--)
{
ps->a[i] = ps->a[i - 1];
}
ps->a[pos] = x;
ps->size++;
}
//顺序表删除pos位置的值
void SLpoppos(SeqList* ps, size_t pos)
{
if (pos > ps->size || pos < 0)
{
return 0;
}
for (size_t i = pos; i < ps->size; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
因为线性表存在明显的内存大小不合适的问题,所以引出另外一种存储结构,链表
链表:存储的内容地址不是连续的,随时可以开辟空间与前面的链接在一起
链表的链接是通过结构体内存下一个结构体的指针实现的
以下为单链表函数节点代码
void SListPrint(SListNode* phead)
{
SListNode* cur = phead;
while (cur)
{
printf("%d->", cur->date);
cur = cur->next;
}
printf("NULL\n");
}
SListNode* BuySLNode(SLTDateTpype x)
{
SListNode* newcode = (SListNode*)malloc(sizeof(SListNode));
if (newcode == NULL)
{
perror("malloc file");
return 1;
}
newcode->date = x;
newcode->next = NULL;
return newcode;
}
void SListPushFront(SListNode** pphead, SLTDateTpype x)
{
assert(pphead);
SListNode* newnode = BuySLNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SListPushBack(SListNode** pphead, SLTDateTpype x)
{
assert(pphead);
SListNode* newnode = BuySLNode(x);
SListNode* tail = *pphead;
if (tail == NULL)//我要改变的是结构体指针的数据,所以使用的是指向结构体指针的指针,而不是用结构体指针变量改
{
*pphead = newnode;
}
else
{
while (tail->next != NULL)
{
tail = tail->next;
}
newnode->next = NULL;
tail->next = newnode;
}
}
void SListPopFront(SListNode** pphead)
{
assert(pphead);
//1.温柔的
//if (*pphead == NULL)
//{
// return;
//}
//2.严格的
assert(*pphead != NULL);
SListNode* cur = *pphead;
*pphead = (*pphead)->next;
free(cur);
cur = NULL;
}
void SListPopBack(SListNode** pphead)
{
assert(pphead);
//1.温柔的
//if (*pphead == NULL)
//{
// return;
//}
//2.严格的
assert(*pphead != NULL);
SListNode* tail = *pphead;
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//1.两个指针实现
//SListNode* cur = tail;
//while (tail->next != NULL)
//{
// cur = tail;
// tail = tail->next;
//}
//free(tail);
//cur->next = NULL;
//2.一个指针实现
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
SListNode* SListFind(SListNode* phead, SLTDateTpype x)
{
SListNode* pos = phead;
while (pos)
{
if (pos->date == x)
{
return pos;
}
pos = pos->next;
}
return NULL;
}
void SListDestory(SListNode** pphead)
{
assert(pphead);
SListNode* cur = *pphead;
*pphead = NULL;
while (cur)
{
SListNode* tmp = cur->next;
free(cur);
cur = tmp;
}
}
void SListInsert(SListNode** pphead, SListNode* pos, SLTDateTpype x)
{
assert(pphead);
assert(pos);
SListNode* newnode = BuySLNode(x);
SListNode* cur = *pphead;
if (pos == *pphead)
{
SListPushFront(pphead, x);
}
else
{
while (cur->next != pos)
{
cur = cur->next;
assert(cur);
}
newnode->next = cur->next;
cur->next = newnode;
}
}
void SListInsertAfter(SListNode** pphead, SListNode* pos, SLTDateTpype x)
{
assert(pphead);
assert(pos);
SListNode* newnode = BuySLNode(x);
SListNode* cur = *pphead;
while (cur != pos)
{
cur = cur->next;
assert(cur);
}
newnode->next = cur->next;
cur->next = newnode;
}
void SListEraseAfter(SListNode** pphead, SListNode* pos)
{
assert(pphead);
assert(pos);
SListNode* cur = *pphead;
while (cur != pos)
{
cur = cur->next;
assert(cur);
}
assert(cur->next);
SListNode* tmp = cur->next;
cur->next = cur->next->next;
free(tmp);
}
void SListErase(SListNode** pphead, SListNode* pos)
{
assert(pphead);
assert(pos);
SListNode* cur = *pphead;
SListNode* curs = cur;
if (*pphead == pos)
{
free(pos);
*pphead = NULL;
}
else
{
while (cur->next != pos)
{
curs = cur;
cur = cur->next;
assert(cur);
}
curs->next = pos;
free(cur);
}
}
需要注意:如果是为了改变结构体的地址,那么就要用结构体的指针。即改变什么,就传入什么的指针,以上代码在头删或者头增时要考虑phead的指针,因为要改变结构体指针,所以传入的指针是二级指针指向的结构体指针
当然有其他的方法避免改变结构体指针:第一种加一个哨兵头,哨兵头指的是不用于参与其他函数的结构体,它所记录的是后面链表的地址,所以头删不会有它参与,但是它会记录下一个有效的链表地址。第二种是穿出同样的指针,在函数外用相同指针接收。第三种是被包含在一个结构体中,再用该结构体的指针来访问。
但是对比起顺序表,单链表的优势只有头增和头删,该表的尾删尾增和某位置插入删除都需要遍历,为了方便出现了双向链表,该结构体有两个指针元素,存储上一个元素的地址和下一个的元素地址,这样循环起来可以方便的找到任意位置。
以下是双向循环带哨兵头的链表
void SListInit(SList* phead)
{
assert(phead);
phead->next = phead;
phead->prev = phead;
}
SList* BuySListDate(LTDataType x)
{
SList* newnode = (SList*)malloc(sizeof(SList));
newnode->date = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
void SListPrint(SList* phead)
{
assert(phead);
printf("phead<=>");
SList* cur = phead->next;
while (cur != phead)
{
printf("%d<=>", cur->date);
cur = cur->next;
}
printf("\n");
}
void SListPushBack(SList* phead, LTDataType x)
{
assert(phead);
//SList* newnode = BuySListDate(x);
//newnode->prev = phead->prev;
//phead->prev = newnode;
//newnode->next = phead;
//newnode->prev->next = newnode;
ListInsert(phead, x);
}
void SListPushFront(SList* phead, LTDataType x)
{
assert(phead);
//SList* newnode = BuySListDate(x);
//newnode->next = phead->next;
//phead->next->prev = newnode;
//newnode->prev = phead;
//phead->next = newnode;
ListInsert(phead->next, x);
}
void SListPopBack(SList* phead)
{
assert(phead);
//SList* cur = phead->prev;
//phead->prev = cur->prev;
//cur->prev->next = phead;
//free(cur);
ListErase(phead->prev);
}
void SListPopFront(SList* phead)
{
assert(phead);
//SList* cur = phead->next;
//phead->next = cur->next;
//cur->next->prev = phead;
//free(cur);
ListErase(phead->next);
}
SList* ListFind(SList* phead, LTDataType x)
{
assert(phead);
SList* cur = phead->next;
while (cur != phead)
{
if (cur->date == x)
{
return cur;
}
}
return NULL;
}
void ListInsert(SList* pos, LTDataType x)
{
assert(pos);
SList* newnode = BuySListDate(x);
newnode->prev = pos->prev;
pos->prev->next = newnode;
newnode->next = pos;
pos->prev = newnode;
}
void ListErase(SList* pos)
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
}
void ListDestory(SList* phead)
{
SList* cur = phead->next;
while (cur != phead)
{
SList* tmp = cur;
cur = cur->next;
free(tmp);
}
free(phead);
}
顺序表:
缺点:不方便头删和头增,因为都要重新遍历一遍把前面的数往后移;内存的利用率可能低
优点:可以随机访问,因为数组有下标可以访问;另外还会提高cpu的高速缓存区的命中效率高
何为命中效率,指的是高速缓存区为了读取内存中的数据方便会提前将要访问的对象前后读取到高速缓存中等待读取,但是读取有条件即在该数据相邻的地址读取,我们知道顺序表的地址是连续的,但是链表不一定,开辟空间可能有一段距离,所以读取不到只能当运行到相关内容时再访问,这样命中本可以顺手访问的地址没有访问到,导致效率下降。
链表:
缺点:不支持随机访问
优点:充分利用空间