// 顺序表的静态存储
#define N 100
typedef int SLDataType;
typedef struct SeqList
{
SLDataType array[N]; // 定长数组
size_t size; // 有效数据的个数
}SeqList;
// 顺序表的动态存储
typedef struct SeqList
{
SLDataType* array; // 指向动态开辟的数组
size_t size ; // 有效数据个数
size_t capicity ; // 容量空间的大小
}SeqList;
SeqList.h
# pragma once //防止被包含多次,多次展开
#include
#include
#include
typedef int DataType;
typedef struct SeqList
{
DataType* _array;
size_t _size;
size_t _capacity;
}SeqList;
void SeqListInit(SeqList* ps);
void SeqListDestory(SeqList* ps);
void SeqListPushBack(SeqList* ps, DataType x);
void SeqListPushFront(SeqList* ps, DataType x);
void SeqListPopBack(SeqList* ps, DataType x);
void SeqListPopFront(SeqList* ps, DataType x);
void SeqListInsert(SeqList* ps, size_t pos,
DataType x); //O(N),建议少用
void SeqListErase(SeqList* ps, size_t pos,
DataType x); //O(N),建议少用
size_t SeqListSize(SeqList* ps);
size_t SeqListFind(SeqList* ps);
DataType SeqListAt(SeqList* ps, size_t pos);
//pos位置的值
SeqList.c
#include "SeqList.h"
#include
#include
#include
//初始化
void SeqListInit(SeqList* ps) //传结构体指针,否则会有“传值错误”
{
assert(ps); //断言:如果传一个空指针,会提示第几行有问题,可以控制,反之会崩溃,很难解决
ps->_array = NULL;
ps->_size = 0;
ps->_capacity = 0;
}
//销毁
void SeqListDestory(SeqList* ps)
{
assert(ps);
if (ps->_array)
{
free(ps->_array);
ps->_array = NULL;
ps->_size = ps->_capacity = 0;
}
}
//插入
void SeqListCheckCapacity(SeqList* ps)
{
assert(ps);
//满了的话会增容
if (ps->_size == ps->_capacity)
{
size_t newcapacity = (ps->_capacity == 0) ? 4 : ps->_capacity * 2;
//ps->_array = realloc(ps->_array, newcapacity); //会越界(可能因为空间开少了)
ps->_array = realloc(ps->_array, sizeof(DataType)*newcapacity);
//realloc的第一个参数如果为空的话,相当于malloc
ps->_capacity = newcapacity;
}
}
void SeqListPushBack(SeqList* ps, DataType x)
{
assert(ps);
//增容
SeqListCheckCapacity(ps);
ps->_array[ps->_size] = x;
ps->_size++;
}
//头插:时间复杂度为O(N):如果要插入n个数据,则为n^2 ,一般插入情况很多,不建议使用
//将顺序表里原有的数据,从后往前依次往后挪一个位置,最终在第一个位置插入新数据即可
//记最后一个位置为end(为size),前一个位置为end-1,把end-1挪到end
void SeqListPushFront(SeqList* ps, DataType x)
{
assert(ps);
size_t end = ps->_size; //最后一个位置
while (end > 0)
{
ps->_array[end] = ps->_array[end - 1];
--end;
}
ps->_array[0] = x; //插入新的数据
ps->_size++;
}
//尾删
void SeqListPopBack(SeqList* ps)
{
assert(ps && ps->_size > 0);
--ps->_size;
}
//头删
//把数据从第二个(下标为1)到最后一个位置一次往前覆盖一个即可
void SeqListPopFront(SeqList* ps)
{
assert(ps);
/*size_t start = 0;
while (start < ps->_size-1)*/
size_t start = 1;
while (start < ps->_size)
{
ps->_array[start - 1] = ps->_array[start];
++start;
}
--ps->_size;
}
//某个位置插入一个数据:pos位置插入一个x
//先把数据从后往前挪,直至挪到pos的位置,然后插入数据
void SeqListInsert(SeqList* ps, size_t pos,
DataType x) //O(N),建议少用
{
assert(ps);
SeqListCheckCapacity(ps);
越界(截断:char ch = 1; 整形提升:int i = ch; ch是一个字节;int是四个字节,提升成整形,高位补东西(此时要看高位是什么,高位是0(1),全补0(1))),一般发生在赋值阶段或者比较的时候(比较时通常是提升)
//size_t end = ps->_size - 1;
//while (end >= pos)
//{
// ps->_array[end + 1] = ps->_array[end];
// end--;
//}
//ps->_array[pos] = x;
//ps->_size++;
//int end = ps->_size - 1;
//while (end >= (int)pos) //end是int型,有符号,pos是无符号;类型不一样时往表示范围大是类型提升(比如:char和int,char—>int转)//这里会先隐式的转为int型
//{
// ps->_array[end + 1] = ps->_array[end];
// end--;
//}
size_t end = ps->_size;
while (end > pos) //end是int型,有符号,pos是无符号;类型不一样时往表示范围大是类型提升(比如:char和int,char—>int转)//这里会先隐式的转为int型
{
ps->_array[end] = ps->_array[end-1];
end--;
}
ps->_array[pos] = x;
ps->_size++;
}
size_t SeqListSize(SeqList* ps)
{
assert(ps);
return ps->_size;
}
size_t SeqListFind(SeqList* ps, DataType x);
//pos位置的值,可以用来访问数据
DataType SeqListAt(SeqList* ps, size_t pos)
{
assert(ps);
return ps->_array[pos];
}
缺点:
1、在头部或者中间插入、删除数据时,效率很低,(需要挪数据,然后插入或者覆盖),删除时间复杂度是O(N)
2、增容
代价大(开一个更大的空间,再拷贝过去,释放旧空间)
浪费空间(两倍增长:100个数据,若需要插入第101个数据,就会浪费99的空间)
链表物理上不是连续的,独立的,用指针可以链起来
链表有数据域、指针域(链接下一个位置),直至指针为空即结束,前一个节点存储的是下一个节点的地址 ; 用一个申请一个,没有空间浪费
单向链表 :前面一个节点可以找到后一个节点,后一个节点不能找到前一个节点
劣势 :从后往前不好找,以节点为单位存储,不支持随机访问
双向链表
不带头:从第一个节点开始就是有效节点
带头:第一个节点占位,接下来才是有效节点
循环、不循环… (八种)
无头单向非循环链表: 结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多
带头双向循环链表: 结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表
void SListPushBack(SList* plt, SLTDateType x)
{
assert(plt);
//先申请内存空间
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
newnode->_data = x;
newnode->_next = NULL;
//1.为空
if (plt->_head == NULL)
{
plt->_head = x;
}
//2.不为空
else
{
SListNode* cur = plt->_head;
while (cur->_next != NULL)
{
cur = cur->_next;
}
cur->_next = newnode;
}
}
void SListPushFront(SList* plt, SLTDateType x)
{
assert(plt);
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
newnode->_data = x;
newnode->_next = NULL;
//空不空均如此
newnode->_next = plt->_head; //第一个节点的地址存储在头里(plt->_head)
plt->_head = newnode;
}
void SListPopFront(SList* plt, SLTDateType x)
{
assert(plt);
if (plt == NULL)
{
return;
}
else
{
SListNode* cur = plt->_head;
plt->_head = cur->_next;
free(cur);
cur = NULL;
}
}
+找尾:cur->_next->_next == NULL (不能直接将最后一个置为空,这样容易造成原链表倒数第二个节点的_next存在野指针的问题)
void SListPopBack(SList* plt, SLTDateType x)
{
assert(plt);
SListNode* cur = plt->_head;
//为空节点
if (cur == NULL)
{
return;
}
//只有一个节点,刚好删除它
else if (cur->_next == NULL)
{
free(cur);
plt->_head = NULL;
}
//多个节点
else
{
while (cur->_next->_next != NULL)
{
cur = cur->_next;
}
free(cur->_next);
cur->_next = NULL;
}
}
//C++库里,双向链表-list/单链表-forward_list
//单链表的头插头删时间复杂度均为O(1),这是常用的,比如哈希表里的应用
//单链表缺陷偏多,出的题目比较多,陷阱多一些
//头插用双向链表更容易解决;单链表常用于在节点的后面插入
void SListFind(SList* plt, SLTDateType x)
{
assert(plt);
SListNode* cur = plt->_head;
while (cur != NULL)
{
if (cur->_data = x)
{
return cur;
}
cur = cur->_next;
}
return NULL;
}
//newnode得先指向pos的下一个位置,然后pos再指向newnode
void SListInsertAfter(SListNode* pos,
SLTDateType x)
{
assert(pos);
SListNode* newnode =
(SListNode*)malloc(sizeof(SListNode));
newnode->_data = x;
newnode->_next = NULL;
newnode->_next = pos->_next;
pos->_next = newnode;
}
void SListEraseAfter(SListNode* pos)
{
assert(0);
if (pos->_next == NULL)
{
return;
}
else
{
SListNode* next = pos->_next;
pos->_next = next->_next;
free(next);
next = NULL;
}
}
//删除目标值
void SListRemove(SList* plt, SLTDateType x)
{
assert(plt);
SListNode* prev = NULL;
SListNode* cur = plt->_head;
while (cur != NULL)
{
if (cur->_data == x)
{
if (prev == NULL) //头删(cur->_data == x)
{
plt->_head = cur->_next;
}
prev->_next = cur->_next;
free(cur);
cur = NULL;
return;
}
else
{
prev = cur;
cur = cur->_next;
}
}
}
【小结】
链表:以节点为单位存储,不支持随机访问,从后往前不好找。 任意位置插入删除时间复杂度为O(1)
链表是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点的里存到下一个节点的指针。
由于不是必须按照顺序存储的,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表——顺序表快多,但是查找一个节点或者访问特定编号的节点则需要O(N)的时间,而顺序表相应的时间复杂度分别是O(log n)、O(1)
链表结构可以克服数组链表预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理,但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大
链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同类型:单向链表、双向链表、循环链表
链表还可以衍生出循环链表、静态链表、双链表等。对于链表使用,需要注意头结点的使用。