顺序表和链表都是最最基本的数据结构,顺序表是数组形式来实现的,具有连续的物理结构,与逻辑结构。而链表则是在逻辑上连续,物理空间上不连续。
顺序表,利用动态开辟数组空间来实现。那么需要实现的接口有哪些呢。
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int data;
typedef struct seplist
{
data* a;
size_t size;
size_t cap;
} SL;
void slpushfront(SL* ps, int x);
void slpushback(SL* ps,int x);
void slpopfront(SL* ps);
void slpopback(SL* ps);
void initseplist(SL*);
void selpistprint(SL*);
void checksl(SL*);
void slinsert(SL*, int pos, int x);
void slerase(SL*, int pos);
int slfind2(SL*, data x);
void destroy(SL*);
这里首先我们得有增删查改的基本实现。那么我们去思考,如何进行插入呢?
显然,如果尾部插入只需要将数据插入最后,尾删也同样如此,但是头插头删,是需要将数据挪到后面的。那么这样就会造成挪动的时间复杂度。
void initseplist(SL* ps)
{
assert(ps);
ps->cap = 5;
ps->a = (data*)malloc(sizeof(data) *ps->cap);
if (ps->a == NULL)
{
printf("创建失败\n");
exit(-1);
}
ps->size = 0;
}
void checksl(SL* ps)
{
assert(ps);
if (ps->size == ps->cap)
{
ps->cap *= 2;
ps->a = (data*)realloc(ps->a, sizeof(data) * ps->cap);
if (ps->a == NULL)
{
printf("增容失败\n");
exit(-1);
}
}
}
void selpistprint(SL* ps)
{
assert(ps);
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
}
void slpushback(SL* ps, int x)
{
//assert(ps);
//checksl(ps);
//ps->a[ps->size] = x;
//ps->size++;
slinsert(ps, ps->size, x);
}
void slpopback(SL* ps)
{
//assert(ps);
//ps->size--;
slerase(ps, ps->size - 1);
}
void slpushfront(SL* ps, int x)
{
assert(ps);
checksl(ps);
slinsert(ps, 0, x);
/*int end = 0;
end = ps->size;
while (end)
{
ps->a[end] = ps->a[end - 1];
end--;
}
ps->a[0] = x;
ps->size++;*/
}
void slpopfront(SL* ps)
{
/*assert(ps);
int i = 0;
for (i = 0; i < ps->size; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;*/
slerase(ps, 0);
}
void slinsert(SL* ps, int pos, int x)
{
assert(ps);
assert(pos <= ps->size && pos >= 0);
checksl(ps);
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
void slerase(SL* ps, int pos)
{
assert(ps);
assert(pos < ps->size && pos >= 0);
int start = pos;
while (start < ps->size - 1)
{
ps->a[start] = ps->a[start + 1];
start++;
}
ps->size--;
}
//data slfind(SL* ps, int pos)
//{
// assert(ps);
// data i = ps->a[pos - 1];
//
// return i;
//}
int slfind2(SL* ps, data x)
{
assert(ps);
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
return i;
}
printf("没有查到\n");
return 0;
}
void destory(SL* ps)
{
free(ps->a);
ps->a = NULL;
ps->cap = ps->size = 0;
}
但是当我们去,完成任意位置的插入,那么就可以实现复用,那么就大大减少了代码的复杂度,这种思想在以后的实现中也是非常重要的,对重复使用代码的封装和相似代码的复用,可以大大减少代码量以增强可读性。
单链表 实现接口
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SlistDataType;
typedef struct SlistNode
{
SlistDataType data;
struct SlistNode* next;
}SLN;
void Slistpushback(SLN** head, SlistDataType x);
void Slistpushfront(SLN** head, SlistDataType x);
void Slistpopback(SLN** head);
void Slistpopfront(SLN** head);
void Slistprint(SLN* head);
SLN* Slistfind(SLN* head,SlistDataType x);
void insertafter(SLN* pos, SlistDataType x);
void eraseafter(SLN* pos);
那么实现增删查改时,单链表有有何不同呢,首先就是很复杂~
#include "slist.h"
SLN* CreatnewNode(SlistDataType x)
{
SLN* newNode = (SLN*)malloc(sizeof(SLN));
if (newNode == NULL)
{
printf("申请失败\n");
exit(-1);
}
newNode->data = x;
newNode->next = NULL;
return newNode;
}
void Slistprint(SLN* head)
{
SLN* cur = head;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
void Slistpushback(SLN** head, SlistDataType x)
{
SLN* newNode = CreatnewNode(x);
if (*head == NULL)
{
*head = newNode;
}
else
{
SLN* tail = *head;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newNode;
}
}
void Slistpopback(SLN** head)
{
if (*head == NULL)
{
return;
}
else if ((*head)->next == NULL)
{
free(*head);
*head = NULL;
}
else
{
SLN* pre = NULL;
SLN* tail = *head;
while (tail ->next != NULL)
{
pre = tail;
tail = tail->next;
}
pre->next = NULL;
free(tail);
tail = NULL;
}
}
void Slistpushfront(SLN** head, SlistDataType x)
{
SLN* phead = CreatnewNode(x);
if (*head == NULL)
{
*head = phead;
}
else
{
phead->next = *head;
*head = phead;
}
}
void Slistpopfront(SLN** head)
{
if (*head == NULL)
{
return;
}
else if ((*head)->next == NULL)
{
free(*head);
*head = NULL;
}
else
{
SLN* cur = *head;
(*head) = (*head)->next;
free(cur);
cur = NULL;
}
}
SLN* Slistfind(SLN* head,SlistDataType x)
{
while (head)
{
if (head->data == x)
{
return head;
}
head = head->next;
}
return NULL;
}
void insertafter(SLN* pos, SlistDataType x)
{
assert(pos);
if (pos->next == NULL)
return;
SLN* newnode = CreatnewNode(x);
SLN* cur = pos->next;
pos->next = newnode;
newnode->next = cur;
}
void eraseafter(SLN* pos)
{
assert(pos);
if (pos->next == NULL)
return;
SLN* nextnext;
nextnext = pos->next->next;
SLN* next = pos->next;
pos->next = nextnext;
free(next);
next = NULL;
}
首先在增删查改的过程中,一定要小心空指针的解引用,空链表的情况也要讨论到,如像删除的情况下,那么删最后一个也是需要考虑到,那么如果你要改动头指针,那么传的指针也要是二级指针,并且,要保留前一个节点,不然怎么链接呢? 所以写这里的代码时候需要细细推敲细节。一般来说,prev cur next 三指针控制是很好的办法。
双向链表
双向带头循环链表,虽然来说结构很复杂,但是操作比单链表要简单太多了。
如图可见,如果存在一个头节点,那么显然,我们就不用传二级指针,也不用考虑空链表的情况了,加上了循环便可以很容易的找到尾节点,前后指针也可以去找到前面的节点。这样子的结构虽然复杂了,但是操作却简单了许多。
#pragma once
#include <stdio.h>
#include <assert.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
LTDataType data;
}ListNode;
ListNode* init();
ListNode* Creatnode(LTDataType x);
ListNode* printlist(ListNode* head);
void pushback(ListNode* head, LTDataType x);
void popback(ListNode* head);
void pushfront(ListNode* head, LTDataType x);
void popfront(ListNode* head);
ListNode* findlist(ListNode* head, LTDataType x);
void insert(ListNode* pos, LTDataType x);
void erase(ListNode* pos);
void clearlist(ListNode* head);
void destorylist(ListNode** head);
实现
#include "list.h"
ListNode* init()
{
ListNode* head = Creatnode(0);
head->next = head;
head->prev = head;
return head;
}
ListNode* printlist(ListNode* head)
{
assert(head);
ListNode* cur = head->next;
while (cur != head)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
ListNode* Creatnode(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
newnode->next = NULL;
newnode->prev = NULL;
newnode->data = x;
return newnode;
}
void pushback(ListNode* head, LTDataType x)
{
assert(head);
ListNode* newnode = Creatnode(x);
ListNode* tail = head->prev;
tail->next = newnode;
head->prev = newnode;
newnode->next = head;
newnode->prev = tail;
}
void popback(ListNode* head)
{
assert(head);
if (head->next == head)
{
printf("链表无有效值\n");
return head;
}
ListNode* tail = head->prev;
head->prev = tail->prev;
tail->prev->next = head;
free(tail);
}
void pushfront(ListNode* head, LTDataType x)
{
assert(head);
ListNode* first = head->next;
ListNode* newnode = Creatnode(x);
head->next = newnode;
first->prev = newnode;
newnode->prev = head;
newnode->next = first;
}
void popfront(ListNode* head)
{
assert(head);
if (head->next == head)
{
printf("链表无有效值\n");
return head;
}
ListNode* first = head->next;
head->next = first->next;
first->next->prev = head;
free(first);
}
ListNode* findlist(ListNode* head, LTDataType x)
{
ListNode* cur = head->next;
while (cur != head)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
void insert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* newnode = Creatnode(x);
ListNode* posPrev = pos->prev;
posPrev->next = newnode;
newnode->next = pos;
pos->prev = newnode;
newnode->prev = posPrev;
}
void erase(ListNode* pos)
{
assert(pos);
ListNode* posPrev = pos->prev;
ListNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
pos = NULL;
}
void clearlist(ListNode* head)
{
assert(head);
ListNode* cur = head->next;
while (cur != head)
{
ListNode* now = cur;
cur = cur->next;
free(now);
}
}
void destorylist(ListNode** head)
{
clearlist(*head);
free(*head);
*head = NULL;
}
顺序表与链表的优缺点
这里可以有个形象的比喻,一白遮百丑
顺序表的”白“在哪里呢,它可以支持随机访问,因为是由下标来确定的,所以这就是顺序表的最大优势,也是链表的最大劣势,因为随机访问不仅更灵活,还有在寄存器中命中率更高这样效率更高,
而链表的优势即顺序表的劣势,就是空间效率的利用率高,且不用移动数据,链表是要用一个开一个,且就是在哪里开,而数组还需要移动。
但是无论如何,两种数据结构各有优劣,所以可以互补,不一定要最好的,但是一定是最合适的~