线性表的顺序存储结构
线性表插入
思路:
(1)判断线性表是否已经满员,以及插入的位置是否在线性表范围内。
(2)从线性表最后一位开始向后移位,知道要插入的位置。
(3)将此位置赋值为要插入的元素。
#include <stdio.h>
#include <string.h>
typedef enum _BOOL
{
efalse = 0,
etrue = 1
}bool;
#define MAX_SIZE (10)
bool ListInsert(char *p, int i, char e)
{
int k;
int Length =strlen(p);
if (p == NULL) //指针是否为空
{
return efalse;
}
if (Length == MAX_SIZE) //线性表是否已满
{
return efalse;
}
if (i < 0 || i > Length) //插入位置是否在范围内
{
return efalse;
}
for (k = Length; k > i; k--) //向后移位
{
*(p+k) = *(p+k-1);
}
*(p+k) = e;
return etrue;
}
int main(void)
{
char a[MAX_SIZE] = "abcd";
char *str = a;
if (ListInsert(str, 2, 'r'))
{
printf("str: %s", str); // str: abrcd
}
else
{
printf("error");
}
return 0;
}
线性表删除
思路:
(1)判断删除的位置是否在线性表范围内
(2)从删除位后一位开始,向前移位
(3)原本的最后移位赋为’\0’
#include <stdio.h>
#include <string.h>
bool ListDelet(char *p, int i)
{
if (p == NULL)
{
return efalse;
}
int k;
int Length = strlen(p);
if (i < 0 || i > Length - 1)
{
return efalse;
}
for (k = i; k < Length -1; k++)
{
*(p+k) = *(p+k+1);
}
*(p+Length-1) = '\0';
return etrue;
}
int main(void)
{
char a[MAX_SIZE] = "abcd";
char *str = a;
if (ListDelet(str, 1))
{
printf("str: %s", str); //str: acd
}
else
{
printf("error");
}
}
链表
线性表的顺序存储结构,导致每次插入和删除都要移动大量元素,平均时间复杂度为O(n)。
因此引入线性表的链式存储结构,即链表,它是由很多结点构成,每个结点分为数据域和指针域,数据域存储该结点的数据,指针域存储下一个结点的存储地址。我们知道指针可以随意改变指向的地址,这样我们就可以很方便的插入和删除了。
创建链表
C语言中可以使用结构体来表示结点:
typedef struct Node
{
int m_data; //数据
struct Node *next; //指针-下一结点的地址
}Node;
思路:
(1)定义链表的头结点和尾结点;
(2)创建一个结点并为其申请内存;
(3)将数据填入结点中,并将其指针置为NULL;
(4)判断是否是第一个结点,如果是,则将头结点赋值为该结点;
(5)否则将上一结点的指针指向此结点
(6)重新定位尾结点
typedef struct LinkList
{
struct Node *head;
struct Node *end;
}LinkList;
struct LinkList stLinkList = {NULL, NULL}; // 链表(头结点、尾结点)
void CreatList(int data)
{
struct Node *NewNode = (Node *)malloc(sizeof(Node)); //申请内存
NewNode->m_data = data; // 填入数据
NewNode->next = NULL; // 最后一个结点的指针为NULL
if (NULL == stLinkList.head)
{
stLinkList.head = NewNode; // 定位头结点
}
else
{
(stLinkList.end)->next = NewNode; // 在尾部连接新的结点
}
stLinkList.end = NewNode; //重新定位尾结点
}
注:尾结点的指针必须要设置为空指针,否则在读取时,会继续读取指针指向的地址。
扫描链表
思路:
(1)新建一个结点,初始化等于头结点;
(2)当结点不为空时,读取数据;
(3)将此结点赋值为下一个结点。
void ScanList(void)
{
struct Node *List = stLinkList.head;
while (List != NULL)
{
printf("%d\n", List->m_data);
List = List->next;
}
}
查找结点
思路:
(1)和扫描一样,从头结点向尾结点开始扫描
(2)依次判断是否有结点的数据等于要查找的数据
(3)如果找到,则返回该结点,否则返回空指针。
Node* FindNode(int data)
{
struct Node *List = stLinkList.head;
while (List != NULL)
{
if (data == List->m_data)
{
return List;
}
List = List->next;
}
return NULL;
}
插入结点
思路:
(1)创建一结点,初始化为链表的头结点,用作移动结点
(2)当此结点不为空,且位置仍在目标位置的前面,则继续向后面移动结点
(3)如果移动到目标位置,且不是空结点,则在该结点后面开始插入数据
(4)创建一个新的结点,对数据域进行赋值
(5)首先使新的结点的指针指向下一个结点,再使前一个结点的指针指向新的结点(如果颠倒顺序,则在前一个结点更改指针指向时,就找不到下一个结点地址了)
bool InsertList(LinkList List, int i, int data)
{
int j = 1;
struct Node *pt = List.head;
while ((pt != NULL) && (j < i))
{
pt = pt->next;
j++;
}
if ((pt !=NULL) &&(j == i))
{
struct Node *ThisNode = (struct Node *)malloc(sizeof(Node));
ThisNode->m_data = data;
ThisNode->next = pt->next;
pt->next = ThisNode;
return true;
}
else
{
return false;
}
}
删除链表
思路:
(1)遍历链表结点,如果结点不为空,要先找到下一个结点,再删除此结点
(2)结点遍历结束后,将链表的头结点和尾结点指针设置为空指针。
void FreeList(void)
{
struct Node *List = stLinkList.head;
while (List != NULL)
{
struct Node* pt = List;
List = List->next;
free(pt);
}
stLinkList.head = NULL;
stLinkList.end = NULL;
}
静态链表
指针可以很好的操作地址和指向的数据,但是如果不使用指针,能否使用数组来构造链表呢?
答案是可以的,但是因为数组在创建时,其地址和最大长度都是固定的,我们把用数组构造的链表称为静态链表。
思路:
(1)不知道数据存储的地址,但可以通过知道数据在数组中的位置来访问该数据
(2)定义一个结点结构体,其有数据data
和下一个结点的位置next
组成
(3)为了明确链表的头结点在哪里,数组的最后一位不存储数据,其next
成员指向链表的头结点位置
(4)链表的尾结点的next
应该为0,表明这是最后一位;
(5)因为要知道数组中哪些位置是空闲的,我们就必须要构造一个备用链表,每当要添加一个新的结点时,从备用链表中取出一个结点,再连接到链表中即可。因此数组的第一位不存储数据,其next
成员指向备用链表的头结点位置。
下图中的数组,1-5位已经存储数据,数组的最后一位next
为1,表示链表的头结点的位置为第1位,第一位的存储的数据为A,下一位指向2。数组的第0位的next
为6,表示备用链表的头结点位置为第6位。
#define MAX_SIZE (100)
typedef struct Node
{
int data;
int next;
}StaLinkList[MAX_SIZE];
void CreatStaList(StaLinkList List)
{
int i;
for (i = 0; i < MAX_SIZE - 1; i++)
{
List[i].next = i + 1;
}
List[MAX_SIZE-1].next = 0;
}
插入结点
思路:
(1)首先判断链表是否为空链表,以及插入的位置是否在链表范围内;
(2)从备用链表中取出一个结点,并将数据填进去;
(3)通过遍历的方法,找到第i-1个结点;
(4)将新结点插入:先使新结点的next
指向原本的第i个结点,再使第i-1个结点的next
指向此结点。
/* 取出一个空闲结点 */
int GetFreeNode(StaLinkList List)
{
int i = List[0].next;
if (List[0].next)
{
List[0].next = List[i].next;
}
return i;
}
/* 得到链表的长度 */
int GetListLength(StaLinkList List)
{
int i = MAX_SIZE - 1;
int Length = 0;
while (List[i].next)
{
i = List[i].next;
Length++;
}
return Length;
}
/* 向链表插入一个结点,i:插入的位置,data:数据内容 */
bool InsertList(StaLinkList List, int i, int data)
{
int k, Length;
int n = MAX_SIZE - 1;
int j = GetFreeNode(List);
Length = GetListLength(List);
if ((i < 1) || (i > Length + 1) || (j == 0))
{
return efalse;
}
List[j].data = data; //将数据填入新结点List[j]中
for (k = 1; k < i; k++) //找到第i-1个结点
{
n = List[n].next;
}
List[j].next = List[n].next;//将新结点List[j]插入i-1和i之间
List[n].next = j;
return etrue;
}
删除结点
思路:
(1)和插入结点相似,也是找到第i-1个结点;
(2)将第i-1个结点的next指向第i+1个结点,这样就在链表中删除了第i个结点;
(3)需要将删除的结点重新放入备用链表中,一样是先连接该结点与后一个结点(原来备用链表的头结点),再使数组的第0位List[0].next
指向该结点的位置。
bool DeletList(StaLinkList List, int i)
{
int k, Length, obj;
int n = MAX_SIZE - 1;
Length = GetListLength(List);
if ((i < 1) || (i > Length + 1))
{
return efalse;
}
for (k = 1; k < i; k++)
{
n = List[n].next;
}
obj = List[n].next;
List[n].next = List[obj].next; //第i-1个结点的next指向i+1
List[obj].next = List[0].next; //将删除的结点放到备用链表中
List[0].next = obj;
return etrue;
}
双向链表
前面提到的单向链表,都只能从前往后遍历,使得时间复杂度一直为O(n)
,因此引入双向链表,既可以从前往后,也可以从后往前遍历。
结点的定义包括数据域、指后指针域、指前指针域:
typedef struct DulNode
{
int data;
struct DulNode *prior;
struct DulNode *next;
}DulNode;
插入结点
思路:
(1)首先找到插入位置的前一个结点p
,并给新结点s
分配内存;
(2)在插入时,我们需要先建立结点s
与结点p
和结点p->next
的向前连接;
(3)之后必须先连接s
与p->next
的向后连接,最后使p->next
指向s
。
s->prior = p;
(p->next)->prior = s;
s->next = p->next;
p->next = s;
删除结点
思路:
(1)首先找到要删除的结点s
,再找到前置结点p
;
(2)原本p的下一个结点是s
,所以我们要跳过s
,指向s->next
;
(3)原本s->next
上一个结点是p
,所以我们要跳过s
,指向p
;
(4)最后要释放删除的结点。
p = s->prior;
p->next = s->next;
(s->next)->prior = p;
free(s);
循环链表
循环链表就是将链表的头和尾连接起来,尾结点的下一结点就是头结点,在遍历时,从表中的任意结点出发,都可以遍历所有结点。
单链表和双向链表都可以转换成循环链表。
单循环链表
思路:
(1)单链表的最后一个结点的next
指针为空指针,在转换成循环链表时,使其指向头结点;
(2)这里我们就不需要使用头指针了,只需要定义一个尾指针,就可以访问链表。
双向循环链表
思路:
与单循环链表相似,将头结点与尾结点连接起来,尾结点的nex
t指针指向头结点,头结点的prior
指针指向尾结点,同时也只使用尾指针,来表示循环链表。