线性表:由零个或多个数据元素组成的有限序列。
关键点:
- 有限序列
- 第一个元素有且仅有一个前驱结点,最后一个与元素有且仅有一个后继结点,中间元素有一个前驱结点和一个后继结点
- 线性表可以有零个数据元素,称作空表
线性表分为顺序存储结构和链式存储结构
顺序存储结构:
用一段地址连续的存储空间依次存储线性表中的数据结构
物理关系上:
就是在内存中找个初始地址,然后通过占位的方式,把一定的内存空间占领,然后把相同数据类型的数据元素依次存放在这块内存上。
属性:
- 存储空间的起始位置,数组data,它的存储位置就是线性表存储空间的存储位置
- 线性表的最大存储容量:数组的长度 MAXSIZE
- 线性表的当前长度:length
注意:
数组的长度与线性表的当前长度需要区分一下,数组的长度是存放线性表的存储空间的总长度,一般初始化后不变,而线性表的当前长度是线性表中元素的个数,是会变化的。
顺序链表插入操作:
思路:
- 如果插入位置不合理,抛出异常
- 如果线性表长度大于等于数组长度,则抛出异常或动态扩容
- 从最后一个元素开始向前遍历到第 i 个位置,分别将它们都向后移动一个位置
注意:线性表从 1 开始
顺序链表删除操作:
思路:
- 如果删除位置不合理,抛出异常
- 取出删除元素
- 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置
- 表长减一
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度-1*/
Status ListDelete(Sqlite *L, int i, ElemType *e)
{
int k;
if(L -> length == 0)
{
return ERROR;
}
if(i < 1 || i > L -> length)
{
return ERROR;
}
*e = L -> data(i - 1);
if(i < L -> length)
{
for(k = i; k < L -> length; k++)
{
L -> data(k - 1) = L -> data(k);
}
}
L -> length --;
return OK;
}
时间复杂度:
最好情况:插入和删除操作刚好要求在最后一个位置操作,因为不需要移动任何位置,所以此时的时间复杂度为O(1);
最坏情况:如果要插入和删除的位置是第一个元素,那就意味着要移动所有的元素向后或者向前,所以这个时间复杂度为 O(n);
线性表顺序存储结构的优缺点:
线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是 O(1),而在插入或删除时,时间复杂度都是 O(n)。
它比较适合元素个数比较稳定,不经常插入和删除元素,而更多的操作是存取数据的应用
优点:
- 无须为表中元素之间的逻辑关系而增加额外的存储空间
- 可以快速地存取表中任意位置的元素
缺点:
- 插入和删除操作需要移动大量元素
- 当线性表长度变化比较大时,难以确定存储空间的容量
- 容易造成存储空间的 ”碎片”
线性表的链式存储结构:
出现链式存储结构的出现,就是为了解决顺序存储结构的缺点——插入数据和删除数据时需要移动大量元素。
为什么插入和删除元素时需要移动大量元素?
原因在于相邻两元素的存储位置也具有相邻关系,它们在内存中的位置是紧挨着的,中间没有间隙,无法快速的插入和删除。
特点:
- 用一组任意的存储单元存储线性表的数据元素, 这组存储单元可以存在内存中未被占用的任意位置
- 顺序存储结构每个数据元素只需要存储一个位置就可以了,而链式存储结构中,除了要存储数据信息外,还要存储它的后继元素的存储地址(指针)
单链表:
对于线性表来说,总得有个头有个尾,链表也是这样。我们把链表中的第一个结点的存储位置叫做头指针,最后一个结点指为空。
头指针和头结点的异同?
头指针:
- 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
- 头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字)
- 无论链表是否为空,头指针均不为空
- 头指针是链表的必要元素
头结点:
- 为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以用来存放链表的长度)
- 有了它头结点,对在第一元素结点前插入结点和删除第一结点的操作和其他结点的操作就统一了。
- 头结点不一定是链表的必要元素
单链表的读取:
思路:
- 声明一个结点 p 指向链表第一个结点,初始化 j 从 1 开始
- 当 j < i 时,就遍历链表,让 p 的指针向后移动,不断指向下一结点, j + 1
- 若到链表末尾 p 为空,则说明第 i 个元素不存在
- 查找成功,返回结点 p 的数据
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度-1*/
Status GetElem(LinkList *L, int i, ElemType *e)
{
int k;
LinkList p;
p = L -> next;
j = 1;
while(p && j < 1)
{
p = p -> next;
++j;
}
if(!p || j > i)
{
return ERROR;
}
*e = p -> data;
return OK;
}
单链表的插入:
思路:
- 声明一结点 p 指向链表头结点,初始化 j 从 1 开始
- 当 j < i 时,就遍历链表,让 p 的指针向后移动,不断指向下一结点, j ++
- 若到链表末尾 p 为空,则说明第 i 个元素不存在
- 否则查找成功,在系统中生成一个空结点 s
- 将数据元素 e 赋值给 s -> data
- 单链表的两条插入语句
- 返回成功
注意:顺序不能错
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度-1*/
Status GetElem(LinkList *L, int i, ElemType *e)
{
int k;
LinkList p,s;
p = *L;
j = 1;
while(p && j < 1)
{
p = p -> next;
j++;
}
if(!p || j > i)
{
return ERROR;
}
s = (LinkList)malloc(sizeof(Node));
s -> data = e;
s -> next = p -> next;
p -> next = s;
return OK;
}
单链表的
删除:
思路:
- 声明结点 p 指向链表第一个结点,初始化 j = 1
- 当 j < 1 时, 就遍历链表, 让 p 的指针向后移动,不断指向下一节点, j++
- 若到链表末尾 p 为空,则说明第 i 个元素不存在
- 否则查找成功,将欲删除的结点 p -> next 赋值给 q
- 单链表删除语句:p -> next = q -> next
- 将 q 结点中的数据赋值给 e,作为返回值
- 释放 q 结点
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度-1*/
Status GetElem(LinkList *L, int i, ElemType *e)
{
int k;
LinkList p,q;
p = *L;
j = 1;
while(p && j < 1)
{
p = p -> next;
j++;
}
if(!p || j > i)
{
return ERROR;
}
q = p -> next;
p -> next = q -> next;
*e = q -> data;
free(q);
return OK;
}
效率对比:
- 在插入和删除操作上,链表与顺序存储都由两部分组成,第一:遍历查找第 i 个元素,第二:实现插入和删除元素
- 它们的时间复杂度都是 O(n)
- 因为我们无法直接知道单链表中第 i 个元素的位置,所以在插入和删除时都须遍历链表,并无优势可言
- 如果我们通过遍历链表知道 i 元素的地址后, 在其后进行插入或删除操作时,时间复杂度就为O(1)(单链表用于插入和删除操作较频繁时使用)
单链表的整表创建:
思路:
- 声明一结点 p 和计数器变量 i
- 初始化一空链表 L
- 让 L 的头结点的指针指向 NULL ,即建立一个带头结点的单链表
- 循环实现后继结点的赋值和插入
头插法建表:
先从一个空表开始,生成新结点,读取数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,直到结束为止
简单说:
- 先让新结点的 next 指向头结点之后
- 然后让表头的 next 指向新结点
/*头插法建立单链表示例*/
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node));
(*L) -> next = NULL;
for(i = 0; i < n; i++)
{
p = (LinkList)malloc(sizeof(Node));
p -> data = rand() % 100 + 1;
p -> next = (*L) -> next;
(*L) -> next = p;
}
}
尾插法建表:
/*尾插法建立单链表示例*/
void CreateListTail(LinkList *L, int n)
{
LinkList p, r;
int i;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node));
r = *L;
for(i = 0; i < n; i++)
{
p = (LinkList)malloc(sizeof(Node));
p -> data = rand() % 100 + 1;
r -> next = p;
r = p;
}
r -> next = NULL;
}
单链表的
整表删除:
思路:
- 声明结点 p 和 q
- 将第一个结点赋值给 p,下一结点赋值给 q
- 循环执行释放 p 和将 q 赋值给 p 的操作
Status ClearList(LinkList *L)
{
LiskList p, q;
p = (*L) -> next;
while(p)
{
q = p -> next;
free(p);
p = q;
}
(*L) -> next = NULL;
return OK;
}
单链表结构与顺序存储结构的
优缺点:
- 存储分配方式
- 顺序表用一段连续的存储单元一次存储线性表的数据元素
- 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
2. 时间性能
(1) 查找
- 顺序存储结构:O(1)
- 单链表:O(n)
(2) 插入和删除
- 顺序存储结构需要平移表长一半的元素,时间复杂度:O(n)
- 单链表在计算出元素位置后,插入和删除元素,时间复杂度:O(1)
3. 空间性能
- 顺序存储结构需要预分配存储空间,分大分小都是不合理的
- 单链表无需分配存储空间,元素个数不受限制
静态链表的插入操作:
静态链表相当于是用一个数组来实现线性表的链式存储结构
游标 | 6 | 5 | 3 | 4 | 5 | 2 | 7 | …… | 1 |
数据 | A | C | D | E | B | …… | |||
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | …… | 999 |
#define MAX_SIZE = 1000 ;
typedef struct
{
ElemType data ;
int cur ;
}Component , StaticLinkList[MAX_SIZE];
Status InitList(StaticLinkList L)
{
for(int i= 0 ; i < MAX_SIZE-1 ; ++i)
- {
- space[i].cur = i+1 ;
- }
- space[MAX_SIZE-1].cur = 0 ;
- return OK ;
}
//获取空闲分量的坐标
int Malloc_SSL(StaticLinkList L )
- {
int i = space[MAX_SIZE-1].cur ; //得到第一个结点的下标
if(space[0].cur) //如果存在备用链表
- {
- i = space[0].cur ; //得到备用链表的第一个结点的下标
- }
- space[0].cur = space[i].cur ; //备用结点的第一个结点将被使用,于是备用结点小标往后一个结点移动
- return i ;
- }
//插入新结点操作:
Status InserList(StaticLinkList L , int i , ElemType e)
- {
- int k = MAX_SIZE-1 ;
- if( i < 1 || i >Length(L)+1)
- return ERROR ;
- int j = Malloc_SSL(L) ; //得到备用链表的第一个元素
if (j) //如果元素存在
- {
- L[j].data = e ;
- for(int m = 1 ; m < i ; ++m) //得到第i-1个元素的下标
- k = L[k].cur ;
- L[j].cur = L[k].cur ; //将第i-1个元素的cur设置为新加的这个结点的下标,将新加的这个结点的下标设置为之前第i-1个元素存储的cur值
- L[k].cur = j ;
return OK ;
}
return ERROR ;
}
//删除静态链表中的元素:
Status DeleteLinkList(StaticLinkList L , int i )
- {
- if(i < 1 || i > ListLength(L))
- return ERROR ;
- int k = MAX_SIZE - 1 ;
for(int j = 1 ; j < i ; ++j) //找到第i-1个元素
- {
- k = L[k].cur ;
- }
j = L[K].cur ; //得到第i个元素的下标
L[k].cur = L[j].cur ; //将第i个元素存储的cur值赋值给第i-1个元素的cur
Free_SSL(L , j); //释放掉第i个元素,第i个元素的下标为j
return OK;
}
//其中Free_SSL就是将该下标的结点放到备用链表中去。
void Free_SSL(StaticLinkList L , int i )
- {
- L[i].cur = space[0].cur ; //将之前的备用链表的第一个结点的小标存入到L[i]的cur中
space[0].cur = i ; //下标为i的元素成为备用链表的第一个结点
- }
静态链表的优缺点:
优点:
在插入和删除操作时,只要修改游标,不需要移动元素,改进在顺序存储结构中插入和删除操作的缺陷
缺点:
- 没有解决连续存储分配(数组)带来的表长难以确定的问题
- 失去了顺序存储结构随机存取的特性
腾讯面试题:
题目:
快速找到未知长度单链表的中间结点
解法一:先遍历一遍单链表确定其长度 L,再从头结点出发遍历 L / 2 次找到单链表的中间结点
解法二:快慢指针
设置两个指针 *search, *mid 都指向单链表的头结点, *search 的移动速度是 *mid 的 2 倍。当 *search 指向末尾结点时,*mid 正好就在中间了。
Status GetMidNode(LinkList L, ElemType *e)
{
LinkList search, mid;
mid = search = L;
while(search -> next != NULL)
{
if(search -> next -> next != NULL)
{
search = search -> next -> next;
mid = mid -> next;
}
else
{
search = search -> next;
}
}
*e = mid -> data;
return OK;
}
循环链表:
循环链表是一种首尾相接的链表
判断空链表的条件是 head == head -> next
#include <stdio.h>
#include <stdlib.h>
/*存储结构的定义*/
typedef struct CLinkList {
int data ;
struct CLinkList * next ;
}node;
int main()
{
node * pHead = NULL ;
char opp;
int find;
printf("\n1.初始化链表 \n2.插入结点 \n3.删除结点 \n4.返回结点位置 \n5.遍历链表 \n0.退出 \n请选择你的操作:\n");
while(opp != '0'){
scanf("%c",&opp);
switch(opp){
case '1':
ds_init(&pHead);
printf("\n");
ds_traverse(pHead) ;
break;
case '2':
printf("输入需要插入结点的位置?");
scanf("%d", &find);
ds_insert(&pHead,find) ;
printf("在位置%d插入值后:\n", find) ;
ds_traverse(pHead) ;
printf("\n");
break;
case '3':
printf("输入需要删除的结点位置?");
scanf("%d", &find);
ds_delete(&pHead,find) ;
printf("删除第%d个结点后:\n", find) ;
ds_traverse(pHead) ;
printf("\n");
break;
case '4':
printf("你要查找倒数第几个结点的值?");
scanf("%d", &find);
printf("元素%d所在位置:%d\n", find, ds_search(pHead,find)) ;
//ListTraverse(L);
printf("\n");
break;
case '5':
ds_traverse(pHead) ;
printf("\n");
break;
case '0':
exit(0);
}
}
return 0 ;
}
/************************************************************************/
/* 操作 */
/************************************************************************/
/*初始化循环链表*/
void ds_init(node **pNode) {
int item ;
node *temp ;
node *target ;
printf("输入结点的值,输入0完成初始化\n");
while(1) {
scanf("%d",&item) ;
fflush(stdin) ;
if(item == 0)
return ;
if((*pNode) == NULL) { /*循环链表中只有一个结点*/
*pNode = (node*)malloc(sizeof(struct CLinkList)) ;
if(!(*pNode))
exit(0) ;
(*pNode)->data = item ;
(*pNode)->next = *pNode ;
}
else {
/*找到next指向第一个结点的结点*/
for(target = (*pNode) ; target->next != (*pNode) ; target = target->next)
;
/*生成一个新的结点*/
temp = (node *) malloc(sizeof(struct CLinkList)) ;
if(!temp)
exit(0) ;
temp->data = item ;
temp->next = *pNode ;
target->next = temp ;
}
}
}
/*插入结点*/
/*参数:链表的第一个结点,插入的位置*/
void ds_insert(node ** pNode ,int i) {
node * temp ;
node * target ;
node * p ;
int item ;
int j = 1 ;
printf("输入要插入结点的值:");
scanf("%d",&item) ;
if(i == 1) { //新插入的结点作为第一个结点
temp = (node *)malloc(sizeof(struct CLinkList)) ;
if(!temp)
exit(0) ;
temp ->data = item ;
/*寻找到最后一个结点*/
for(target = (*pNode) ; target->next != (*pNode) ; target = target->next) ;
temp->next = (*pNode) ;
target->next = temp ;
*pNode = temp ;
}
else {
target = *pNode ;
for( ; j < (i-1) ; target=target->next,++ j) ;
temp = (node *)malloc(sizeof(struct CLinkList)) ;
if(!temp)
exit(0) ;
temp ->data = item ;
p = target->next ;
target->next = temp ;
temp->next = p ;
}
}
/*删除结点*/
void ds_delete(node ** pNode,int i) {
node * target ;
node * temp ;
int j = 1 ;
if(i == 1) { //删除的是第一个结点
/*找到最后一个结点*/
for(target = *pNode ; target->next != *pNode ;target = target->next) ;
temp = *pNode ;
*pNode = (*pNode)->next ;
target->next = *pNode ;
free(temp) ;
}
else {
target = *pNode ;
for( ; j < i-1 ; target= target->next,++j) ;
temp = target->next ;
target->next = temp->next ;
free(temp) ;
}
}
/*返回结点所在位置*/
int ds_search(node * pNode,int elem) {
node * target ;
int i = 1 ;
for(target = pNode ; target->data != elem && target->next != pNode ; ++i , target = target->next) ;
if(target->next == pNode) /*表中不存在该元素*/
return 0 ;
else
return i ;
}
/*遍历*/
void ds_traverse(node * pNode) {
node * temp ;
temp = pNode ;
printf("***********链表中的元素******************\n");
do {
printf("%4d ",temp->data) ;
}while((temp = temp->next) != pNode) ;
printf("\n") ;
}
双向链表:
(1)在数据结构中具有双向指针
(2)插入数据的时候需要考虑前后的方向的操作
(3)同样,删除数据的是有也需要考虑前后方向的操作
typedef struct _DOUBLE_LINK_NODE
{
int data;
struct _DOUBLE_LINK_NODE* prev;
struct _DOUBLE_LINK_NODE* next;
}DOUBLE_LINK_NODE;
DOUBLE_LINK_NODE* create_double_link_node(int value)
{
DOUBLE_LINK_NODE* pDLinkNode = NULL;
pDLinkNode = (DOUBLE_LINK_NODE*)malloc(sizeof(DOUBLE_LINK_NODE));
assert(NULL != pDLinkNode);
memset(pDLinkNode, 0, sizeof(DOUBLE_LINK_NODE));
pDLinkNode->data = value;
return pDLinkNode;
}
void delete_all_double_link_node(DOUBLE_LINK_NODE** pDLinkNode)
{
DOUBLE_LINK_NODE* pNode;
if(NULL == *pDLinkNode)
return ;
pNode = *pDLinkNode;
*pDLinkNode = pNode->next;
free(pNode);
delete_all_double_link_node(pDLinkNode);
}
DOUBLE_LINK_NODE* find_data_in_double_link(const DOUBLE_LINK_NODE* pDLinkNode, int data)
{
DOUBLE_LINK_NODE* pNode = NULL;
if(NULL == pDLinkNode)
return NULL;
pNode = (DOUBLE_LINK_NODE*)pDLinkNode;
while(NULL != pNode){
if(data == pNode->data)
return pNode;
pNode = pNode ->next;
}
return NULL;
}
STATUS insert_data_into_double_link(DOUBLE_LINK_NODE** ppDLinkNode, int data)
{
DOUBLE_LINK_NODE* pNode;
DOUBLE_LINK_NODE* pIndex;
if(NULL == ppDLinkNode)
return FALSE;
if(NULL == *ppDLinkNode){
pNode = create_double_link_node(data);
assert(NULL != pNode);
*ppDLinkNode = pNode;
(*ppDLinkNode)->prev = (*ppDLinkNode)->next = NULL;
return TRUE;
}
if(NULL != find_data_in_double_link(*ppDLinkNode, data))
return FALSE;
pNode = create_double_link_node(data);
assert(NULL != pNode);
pIndex = *ppDLinkNode;
while(NULL != pIndex->next)
pIndex = pIndex->next;
pNode->prev = pIndex;
pNode->next = pIndex->next;
pIndex->next = pNode;
return TRUE;
}
STATUS delete_data_from_double_link(DOUBLE_LINK_NODE** ppDLinkNode, int data)
{
DOUBLE_LINK_NODE* pNode;
if(NULL == ppDLinkNode || NULL == *ppDLinkNode)
return FALSE;
pNode = find_data_in_double_link(*ppDLinkNode, data);
if(NULL == pNode)
return FALSE;
if(pNode == *ppDLinkNode){
if(NULL == (*ppDLinkNode)->next){
*ppDLinkNode = NULL;
}else{
*ppDLinkNode = pNode->next;
(*ppDLinkNode)->prev = NULL;
}
}else{
if(pNode->next)
pNode->next->prev = pNode->prev;
pNode->prev->next = pNode->next;
}
free(pNode);
return TRUE;
}
int count_number_in_double_link(const DOUBLE_LINK_NODE* pDLinkNode)
{
int count = 0;
DOUBLE_LINK_NODE* pNode = (DOUBLE_LINK_NODE*)pDLinkNode;
while(NULL != pNode){
count ++;
pNode = pNode->next;
}
return count;
}
void print_double_link_node(const DOUBLE_LINK_NODE* pDLinkNode)
{
DOUBLE_LINK_NODE* pNode = (DOUBLE_LINK_NODE*)pDLinkNode;
while(NULL != pNode){
printf("%d\n", pNode->data);
pNode = pNode ->next;
}
}