介绍一下链表和顺序表是什么?链表和顺序表的在内存中是如何存储数据的?他们的区别是什么?
顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般顺序表就是以数组的形式实现的。在数组上完成数据的增删查改。数组在内存中的物理地址是连续的。
顺序表分为:
- 静态顺序表:使用定长数组存储元素。
int main() {
//数组元素是规定死的,不可以改变。
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
return 0;
}
- 动态开辟的顺序表
C语言中动态开辟内存一般用malloc和calloc函数开辟,realloc函数增容,最后用free函数释放。可以实现一个通讯录。
typedef int SLDateType;
typedef struct SeqList {
SLDateType* a;
//size为元素个数
size_t size;
//capacity为开辟的空间大小
size_t capacity;
}SeqList;
//初始化顺序表
void SeqListIntit(SeqList* ps) {
assert(ps);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
//释放顺序表申请的空间
void SeqListDestroy(SeqList* ps) {
assert(ps);
if (ps->a)
{
free(ps->a);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
}
//打印顺序表的内容
void SeqListPrint(SeqList* ps) {
assert(ps);
size_t i = 0;
for (i = 0; i < ps->size; i++) {
printf("%d ", ps->a[i]);
}
printf("\n");
}
//检查开辟的空间是否足够,如果不够进行扩容,空间足够什么都不做
void static check_capaticy(SeqList* ps) {
assert(ps);
if (ps->capacity == ps->size) {
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDateType* tmp = (SLDateType*)realloc(ps->a, newCapacity * sizeof(SLDateType));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
ps->a = tmp;
ps->capacity = newCapacity;
}
}
//顺序表的尾插
void SeqListPushBack(SeqList* ps, SLDateType x) {
check_capaticy(ps);
ps->a[ps->size] = x;
ps->size++;
}
//顺序表的头插
void SeqListPushFront(SeqList* ps, SLDateType x) {
assert(ps);
check_capaticy(ps);
int end = ps->size-1;
while (end >= 0) {
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
}
//顺序表的头删
void SeqListPopFront(SeqList* ps) {
if (ps->size > 0) {
int begin = 0;
while (begin < ps->size-1) {
ps->a[begin] = ps->a[begin + 1];
begin++;
}
ps->size--;
}
}
//尾删
void SeqListPopBack(SeqList* ps) {
if (ps->size > 0) {
ps->size--;
}
}
//查找元素,找到返回,不存在返回-1
int SeqListFind(SeqList* ps, SLDateType x) {
int i = 0;
for (i = 0; i < ps->size; i++) {
if (ps->a[i] == x) {
return i + 1;
}
}
return -1;
}
//在下标pos位置插入数据x
void SeqListInsert(SeqList* ps, int pos, SLDateType x) {
assert(ps->a);
assert(pos >=0&&pos<=ps->size);
check_capaticy(ps);
int end = ps->size ;
while (pos < end) {
ps->a[end] = ps->a[end - 1];
end--;
}
ps->a[pos] = x;
ps->size++;
}
//删除pos位置的元素
void SeqListErase(SeqList* ps, int pos) {
assert(ps->a);
assert(pos >= 0 && pos < ps->size);
int mod = pos;
while (mod < ps->size-1) {
ps->a[mod] = ps->a[mod + 1];
mod++;
}
ps->size--;
}
链表
链表和顺序表有所不同,它在内存中会有一个空间存储数据,另一个空间存储下一个节点的值。
链表是一种物理上存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针连接次序实现的。
单链表
- 从上图可以看出,链式结构在逻辑上是最连续的,但是在物理上不一定连续
- 现实中的节点一般都是在堆上申请出来的
- 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
- 如果是最后一个元素,那么存放下一个地址的空间为NULL;
用代码构建一个单链表
typedef int SLDataType;
typedef struct SListNode
{
SLDataType data;
struct SListNode* next;
}SLTNode;
//申请一个链表空间
SLTNode* BuySLTNode(SLDataType x) {
SLTNode* s = (SLTNode*)malloc(sizeof(SLTNode));
if (s == NULL) {
perror("malloc fail");
exit(-1);
}
s->data = x;
s->next = NULL;
return s;
}
//创建一个单链表,返回头节点
SLTNode* CreateSList(int n) {
SLTNode* phead = NULL, *ptail = NULL;
for (int i = 0; i < n; i++) {
SLTNode* newnode = BuySLTNode(i + 10);
if (phead == NULL) {
phead = ptail = newnode;
}
else {
ptail->next = newnode;
ptail = newnode;
}
}
return phead;
}
//打印单链表中的数据
void SLTPrint(SLTNode* Phead) {
while (Phead != NULL) {
printf("%d ", Phead->data);
Phead = Phead->next;
}
printf("NULL\n");
}
//单链表尾插
void SLTPushBack(SLTNode** pphead, SLDataType x) {
if (*pphead == NULL) {
*pphead = BuySLTNode(x);
}
else {
SLTNode* ptail = *pphead;
while (ptail->next != NULL) {
ptail = ptail->next;
}
ptail->next = BuySLTNode(x);
}
}
//单链表的尾删
void SLTPopBack(SLTNode** pphead) {
assert(*pphead);
SLTNode* ptail = *pphead;
if (ptail->next == NULL) {
free(*pphead);
*pphead = NULL;
}
else {
SLTNode* prve = NULL;
while (ptail->next != NULL) {
prve = ptail;
ptail = ptail->next;
}
free(ptail);
prve->next = NULL;
}
}
//单链表的头插
void SLTPushFront(SLTNode** pphead, SLDataType x) {
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//单链表的头删
void SLTPopFront(SLTNode** pphead) {
assert(*pphead);
SLTNode* head = (*pphead)->next;
free(*pphead);
*pphead = head;
}
//查找元素,返回地址
SLTNode* SLTFind(SLTNode* phead, SLDataType x) {
SLTNode* cur = phead;
while (cur) {
if (cur->data == x) {
return cur;
}
cur = cur->next;
}
return NULL;
}
//指针pos后面插入一个链表节点
void SLTInsertAfter(SLTNode* pos, SLDataType x) {
assert(pos);
SLTNode* next = pos->next;
pos->next = BuySLTNode(x);
pos->next->next = next;
}
//pos位置前插入一个节点
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x) {
assert(pos);
if (*pphead == pos) {
SLTPushFront(pphead, x);
}
else {
SLTNode* newnode = *pphead;
while (newnode->next != pos) {
newnode = newnode->next;
}
newnode->next=BuySLTNode(x);
newnode->next->next = pos;
}
}
//删除pos位置的后一个节点
void SLTEraseAfter(SLTNode* pos) {
assert(pos);
if (pos->next != NULL) {
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
}
//释放整个链表
void SLTDestory(SLTNode** pphead) {
SLTNode* cur = *pphead;
while (cur) {
SLTNode* nextnode = cur->next;
free(cur);
cur = nextnode;
}
*pphead = NULL;
}
带头双向循环链表
结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然复杂,但是使用代码实现以后就会发现结构会带来很多优势,实现反而简单,后面看代码实现就会了解。
这个结构每个节点会有三个空间,一个存放数据,一个存放前面节点的地址,一个存放后一个节点的地址。
typedef int LTDataType;
typedef struct ListNode {
//存放数据
LTDataType data;
//指向后一个节点
struct ListNode* next;
//指向前一个节点
struct ListNode* prev;
}ListNode;
//创建链表的头节点
ListNode* ListCreate() {
ListNode* head = (ListNode*)malloc(sizeof(ListNode));
if (head != NULL) {
//如果只有头节点,next和prev指向自己
head->next = head;
head->prev = head;
return head;
}
return NULL;
}
//申请开辟链表节点
ListNode* BuyListNode(LTDataType x) {
ListNode* head = (ListNode*)malloc(sizeof(ListNode));
if (head != NULL) {
head->data = x;
head->next = NULL;
head->prev = NULL;
return head;
}
return NULL;
}
//初始化链表
ListNode* ListInit() {
ListNode* head = BuyListNode(-1);
if (head!=NULL) {
head->next = head;
head->prev = head;
return head;
}
return NULL;
}
//链表的尾插
void ListPushBack(ListNode* phead, LTDataType x) {
assert(phead);
/*ListNode* newnode = BuyListNode(x);
ListNode* cur = phead->prev;
cur->next = newnode;
newnode->prev = cur;
newnode->next = phead;
phead->prev = newnode;*/
ListInsert(phead,x);
}
//打印链表
void ListPrint(ListNode* phead) {
ListNode* cur = phead->next;
while (cur != phead) {
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
//链表的尾删
void ListPopBack(ListNode* phead) {
assert(phead);
if (phead != phead->next) {
/*ListNode* cur = phead->prev;
phead->prev = cur->prev;
cur->prev->next = phead;
free(cur);*/
ListErase(phead->prev);
}
else {
return;
}
}
//链表的头插
void ListPushFront(ListNode* phead, LTDataType x){
/*ListNode* cur = phead->next;
ListNode* newnode = BuyListNode(x);
newnode->next = cur;
cur->prev = newnode;
phead->next = newnode;
newnode->prev = phead;*/
ListInsert(phead->next, x);
}
//链表的头删
void ListPopFront(ListNode* phead) {
assert(phead);
if (phead != phead->next) {
/*ListNode* cur = phead->next;
phead->next = cur->next;
cur->next->prev = phead;
free(cur);*/
ListErase(phead->next);
}
else {
return;
}
}
//查找数据,找到返回地址,找不到返回NULL
ListNode* ListFind(ListNode* phead, LTDataType x) {
assert(phead);
ListNode* pos = phead->next;
while (pos != phead) {
if (pos->data == x) {
return pos;
}
pos = pos->next;
}
return NULL;
}
//链表的销毁
void ListDestory(ListNode* phead) {
assert(phead);
ListNode* tail = phead->next;
while (tail != phead) {
ListNode* next = tail->next;
free(tail);
tail = next;
}
free(phead);
}
//在pos节点之前插入一个节点
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* newnode = BuyListNode(x);
ListNode* prev = pos->prev;
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
//删除pos节点
void ListErase(ListNode* pos) {
assert(pos);
ListNode* next = pos->next;
ListNode* prev = pos->prev;
prev->next = next;
next->prev = prev;
free(pos);
}
顺序表和链表的区别
顺序表:
优点:尾插尾删效率高,下标的随机访问快
缺点:空间不够需要扩容(扩容代价大);头部或中间插入删除数据需要挪动数据
链表:
优点:按需申请释放小块节点内存;任意位置插入效率很高;O(1)
缺点:不支持下标的随机访问