线性表
一、线性表的顺序表示1.特点
①逻辑上相邻的元素,物理上也相邻,地址连续
②首元素a1地址为LOC(a1),每个元素占L字节,则LOC(ai+1)=LOC(ai)+L;LOC(ai)=LOC(a0)+L * i;
③顺序表操作:修改、插入(在第i个位置前)、删除、查找、排序
2.法一:静态数组
#define maxlen 100
typedef struct {
int elem[maxlen];//可存放maxlen个元素的数组
int length;
}SeqList;
SqList L;
L.length = n;
//线性表中含有n个元素,第一个元素为elem[0],第n个是L.elem[L.length-1],表的最后一个位置是L.elem[maxlen-1]
3.法二:指针数组
注:所有函数里的参数SqList &L,带&是为了可以真正修改L,不用返回L的函数就不用加&
1.首先,用一个结构体来维护顺序表
#define LIST_INIT_SIZE 100//指针数组初始申请空间的大小
#define LISTINCREMENT 10//指针数组每次扩大空间的大小
#define OK 1
#define OVERFLOW -2
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int* elem;//存储空间基地:该数据得到的内存分配的起始地址,也是数组名
int length;//表中实际含有的元素个数
int listsize;//数组的容量
}SqList;
SqList L;
2.建立一个空的顺序表
int InitList(SqList& L) {
L.elem = (int*)malloc(LIST_INIT_SIZE * sizeof(int));
if (L.elem == 0) {//表示申请空间失败
exit(OVERFLOW);//建立空表失败
}
L.length = 0;//空表含0个元素
L.listsize = LIST_INIT_SIZE;//当前用于存放线性表的数组的容量
return OK;//建立成功
}
3.实现插入元素,这个算法的时间复杂度是O(n)
n个元素,平均移动n/2个元素
int InsertList(SqList& L, int i, int x) {
int j, * newbase;
if (i <1 || i > L.length + 1) return -1;//i的合法值为[1, L.length+1]
if (L.length == L.listsize) {//检查空间是否够用(线性表的长度达到了当前分配的空间大小)
//空间不够用,扩大空间
newbase = (int*)realloc(L.elem, (L.listsize + LISTINCREMENT) * sizeof(int));//扩大后的空间大小
if (newbase == 0) exit(OVERFLOW);//存储分配失败
L.elem = newbase;//新基址
L.listsize = L.listsize + LISTINCREMENT;
}
//将an~ai顺序向下移动,为新元素让出位置
//数组下标是0~L.length-1,分别对应a1~an(ai),n=Length,顺序表的最后一位是L.elem[L.listsize-1],它不一定有元素
for (j = L.length; j >= i; j--) {
L.elem[j] = L.elem[j - 1];
}
L.elem[i - 1] = x;//插入新元素
++L.length;//修改表长
return 1;//插入成功
}
//插入的第二种写法
int InsertList2(SqList& L, int i, int x) {
int* p, * q, *newbase;
if (i<1 || i > L.length + 1)return -1;
if (L.length == L.listsize) {
newbase = (int*)realloc(L.elem, (L.listsize + LISTINCREMENT) * sizeof(int));
if (newbase == 0) exit(OVERFLOW);
L.elem = newbase;
L.listsize = L.listsize + LISTINCREMENT;
}
q = &(L.elem[i - 1]);//q记录要插入的位置。即第i个元素的位置
for (p = &(L.elem[L.length - 1]); p >= q; --p) {//p从后往前找
*(p + 1) = *p;//插入位置及之后的元素右移
}
*q = x;//插入x
++L.length;
return OK;
}
4.实现删除元素,时间复杂度为O(n)
int DeleteList(SqList& L, int i) {
int j;
if (i < 1 || i > L.length) {
printf("不存在第i个元素");
return 0;//要删除的元素不存在
}
for (j = i + 1; j < L.length + 1; j++) {
L.elem[j - 2] = L.elem[j - 1];//将ai+1~an顺序上移,从而删除ai,注意第i个元素是a[i-1]
}
L.length--;//修改表长
return 1;//删除成功
}
实现查找,时间复杂度O(n)
int ListSearch(SqList L, int e) {
int i;
for (i = 1; i <= L.length; i++) {
if (L.elem[i - 1] == e)return i;
}
return -1;//没找到e
}
5.线性表就地逆置
void ReverseList(SqList &L) {
int temp;
for (int i = 0,j=L.length-1; i <= j; i++,j--) {
temp = L.elem[i];
L.elem[i] = L.elem[j];
L.elem[j] = temp;
}
}
6.最后写一个主函数调用这些功能
// 初始化线性表
InitList(L);
//创造线性表2,4,6,8,10
for (int i = 1; i <= 5; i++) {
InsertList(L, i, i * 2);
}
// 打印线性表
for (int i = 1; i <= L.length; i++) {
printf("%d ", L.elem[i - 1]);
}
printf("\n");
// 删除第3个元素
int a = DeleteList(L, 3);
printf("%d\n", a);
// 再次打印线性表
for (int i = 1; i <= L.length; i++) {
printf("%d ", L.elem[i - 1]);
}
printf("\n");
// 查找值为6的元素
int index = ListSearch(L, 6);
if (index != -1) {
printf("找到值为6的元素,序号为%d", index);
}
else {
printf("未找到值为6的元素");
}
return 0;
}
二、线性表的链式表示
1.特点
①地址任意,逻辑相邻不一定物理相邻
②单链表分为不带表头,带表头的,表头结点空着或存特殊信息
③只能顺序存取
④插入和删除操作不需要移动数据
⑤按值查找O(n),和顺序表示的按值查找速度相同;按位置查找O(n),比顺序表示的按位置查找速度慢
⑥线性单链表包括动态链表(指针数据类型)、静态链表(数组)
(一)动态链表
首先认识一下指针类型变量的初始化操作
可以申请空间p = (LinkList)malloc(sizeof(Node));,也可以赋值p = q;
接下来的有些函数,我们写带表头和不带表头两个版本
1.查找结点,时间复杂度O(n)
//带表头
LinkList search(LinkList h, int x) {
LinkList p;
p = h->next;//p是第一个元素的地址
while (p != NULL && p->data != x) {
p = p->next;
}
return p;
}
//不带表头
LinkList search1(LinkList h, int x) {
LinkList p;
p = h;//p是第一个元素的地址
while (p != NULL) {
if (p->data == x) return p;
else p = p->next;
}
return NULL;
}
//查找第i个元素,将值赋给e,带表头
int GetElem_L(LinkList L, int i, int& e) {
LinkList p;
p = L->next;
int j = 1;
while (p && j < i) {
p = p = p->next;
++j;
}
if (!p || j > i) return -1;//第i个元素不存在
e = p->data;//取第i个元素
return 1;
}
2.插入结点
void insert(LinkList& p, int x) {
LinkList s;
s = (LinkList)malloc(sizeof(Node));
s->data = x;
s->next = p->next;
p->next = s;
}
//带头结点,在第i个位置之前插入e
int ListInsert_L(LinkList& L, int i, int e) {
LinkList p = L;
LinkList s;
s = (LinkList)malloc(sizeof(Node));
int j = 0;
while (p && j < i - 1) {
p = p->next;
++j; }//寻找第i-1个结点
if (!p || j > i - 1)return -1;//没找到
s->data = e;
s->next = p->next;
p->next = s;
return 1;//插入成功
}
3.删除结点
//不带表头
void deleter(LinkList& p) {
LinkList q;
if (p->next != NULL)//如果p的直接后继结点存在
{
q = p->next;//用q储存被删结点
p->next = q->next;//删除原来的p->next
free(q);//释放q的空间
//为什么要这样转换一下呢?因为如果直接使p->next=p->next->next,想要释放原来被删的p->next的时候,p->next已经换人了,找不到原来的了
}
}
//带表头,删除值为x的结点
void deleter1(LinkList& h, int x) {
LinkList p, q;
p = h->next;
q = h;
while (p != NULL) {
if (p->data == x) {
q->next = p->next;//要删除的是p
free(p);
}
else
{
q = p;
p = p->next;
//p和q都右移一位
}
}
}
//带头结点,删除第i个元素,由e返回其值
int ListDelete_L(LinkList& L, int i, int& e) {
LinkList p = L;
int j = 0;
while (p->next && j < i - 1) {
p = p->next;
++j;
}//寻找第i个结点,p指向第i-1个结点
if (!(p->next) || j > i - 1)return -1;//删除位置不合理
q = p->next;//要删除的是q
p->next = q->next;//删除q
e = q->data;
free(q);
return 1;//成功
}
拓展:单链表里有很多值为x的结点,都删掉,返回删除的结点数量
整体可调试代码如下
#include <stdio.h>
#include <stdlib.h>
#define NULL 0
#define _CRT_SECURE_NO_WARNINGS
//建立一个头指针为h的,带表头结点的单链表(第一个结点是h,是空的,第二个结点储存第一个元素a1)
typedef struct node {
int data;
struct node* next;
}Node, *LinkList;
LinkList h, p;
Node* q;
//带表头,删除值为x的结点
int deleter1(LinkList& h, int x) {
LinkList p, q, cur;
if (h == NULL) {
return 0; // 空链表,直接返回
}
p = h->next;
q = h;
int count = 0;
while (p != NULL) {
if (p->data == x) {
q->next = p->next;//要删除的是p
cur = p->next;//储存p原来的地址
free(p);
p = cur;
count++;
}
else
{
q = p;
p = p->next;
//p和q都右移一位
}
}
return count;
}
//建立单链表
//尾插法,每次在链表尾插入,顺序读入数据,依次尾结点的直接后继
void Create_L2(LinkList& L, int n) {
LinkList p, s;
int i;
L = (LinkList)malloc(sizeof(Node));
L->next = NULL;//头结点L
s = L;//尾结点s
for (i = 1; i <= n; ++i) {//顺序读入a1~an
p = (LinkList)malloc(sizeof(Node));
scanf_s("%d", &p->data);//输入元素
p->next = NULL;
s->next = p;
s = p;//p是新的尾结点
}
}
// 主函数
int main() {
LinkList L2;
// 创建链表L2
printf("Enter the number of elements for L1: ");
int n;
scanf_s("%d", &n);
Create_L2(L2, n);
// 打印链表L1
printf("\nL2: ");
LinkList p = L2->next; // 跳过头结点
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
// 删除 L2 中值为 7 的结点
int count = deleter1(L2, 7);
printf("\nAfter deleting nodes with value 7 in L2: ");
p = L2->next; // 跳过头结点
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
printf("%d", count);
return 0;
}
//其中,while部分还可以这样写,可以节省一个指针的空间
while (p != NULL) {
if (p->data == x) {
q->next = p->next;//要删除的是p
free(p);
p = q->next;
count++;
}
4.建立单链表,首插法,尾插法,O(n)
//首插法,每次在*头结点*后插入,逆序读入数据,依次做头结点的直接后继
void Create_L1(LinkList& L, int n) {
LinkList p;
int i;
L = (LinkList)malloc(sizeof(Node));
L->next = NULL;//头结点L
for (i = n; i > 0; --i) {//逆序读入a1~an
p = (LinkList)malloc(sizeof(Node));
scanf("%d", &p->data);//输入元素
p->next = L->next;
L->next = p;//相当于把p插在L和NULL之间
}
}
//尾插法,每次在链表尾插入,顺序读入数据,依次尾结点的直接后继
void Create_L2(LinkList& L, int n) {
LinkList p, s;
int i;
L = (LinkList)malloc(sizeof(Node));
L->next = NULL;//头结点L
s = L;//尾结点s
for (i = 1; i <= n; ++i) {//顺序读入a1~an
p = (LinkList)malloc(sizeof(Node));
scanf("%d", &p->data);//输入元素
p->next = NULL;
s->next = p;
s = p;//p是新的尾结点
}
}
5.循环单链表
(1)定义:将单链表的尾结点强行指向头结点,带表头就指向表头,不带表头就指向第一个结点
(2)特点:
①从表中任一结点出发均能找到所有结点
②p为尾结点的条件:p->next == h
③循环单链表为空表的判断条件:h->next == h
④空的带表头结点的循环单链表:h指向自己
题目:创建循环单链表,并就地逆置
#include <stdlib.h>
#define _CRT_SECURE_NO_WARNINGS
/* 链表结点定义 */
typedef struct LNode {
int data; //coef为指数, exp为系数
struct LNode* next;
}LNode, * LinkList;
//建立循环单链表
void CreatetCircleList(LinkList& L, int n)
{
if (n <= 0) return; // 如果节点数量小于等于0,则直接返回
L = (LinkList)malloc(sizeof(LNode)); // 创建头结点
L->next = NULL;
LinkList tail = L; // 尾指针指向头结点
for (int i = 0; i < n; i++)
{
int x;
scanf_s("%d", &x);
LinkList p = (LinkList)malloc(sizeof(LNode)); // 创建新节点
p->data = x;
p->next = NULL;
tail->next = p; // 将新节点插入到链表尾部
tail = p; // 更新尾指针指向新的尾节点
}
// 将链表头尾相连,形成循环单链表
tail->next = L;
}
void reverse(LinkList& L) {
LinkList t = L;
LinkList p = t->next;
LinkList q;
q = p->next;
while (p != L) {
p->next = t;
t = p;
p = q;
q = p->next;
}
L->next = t;
}
int main() {
LinkList L;
CreatetCircleList(L, 5); // 创建循环单链表
printf("原始链表:\n");
LinkList p = L->next;
while (p != L) {
printf("%d ", p->data); // 输出每个节点的数据值
p = p->next;
}
printf("\n");
reverse(L); // 反转链表
printf("逆转后的链表:\n");
p = L->next;
while (p != L) {
printf("%d ", p->data); // 输出每个节点的数据值
p = p->next;
}
printf("\n");
return 0;
}
6.双向链表
(1)定义:单链表的每个结点包含2个指针(prior,data,next),分别指向结点的直接前驱和直接后继
(2)特点:
①从表中任一结点出发均能找到所有结点(沿两个方向的指针即可)
②插入和删除操作,结点的2个指针均要连接上
③头结点的prior->NULL,尾结点的next->NULL
④空的带表头结点的双向链表:prior和next都指向NULL
空的不带表头结点的双向链表:啥也没有
7.双向循环链表
(1)带表头结点非空:h->prior = tail;tail->next = h;
(2)带表头结点的空的循环双链表:h->next = h; h->prior = h;
8.最后写一个主函数调用以上功能
int main() {
LinkList L1, L2;
// 创建链表L1,首插法
printf("Enter the number of elements for L1: ");
int n1;
scanf_s("%d", &n1);
Create_L1(L1, n1);
// 创建链表L2,尾插法
printf("Enter the number of elements for L2: ");
int n2;
scanf_s("%d", &n2);
Create_L2(L2, n2);
// 打印链表L1
printf("\nL1: ");
LinkList p = L1->next; // 跳过头结点
while (p != NULL) {
printf("%d\n", p->data);
p = p->next;
}
// 查找值为 5 的结点并打印
int searchValue = 5;
LinkList result = search(L1, searchValue);
if (result != NULL) {
printf("\nFound node with value %d in L1\n", searchValue);
}
else {
printf("\nNode with value %d not found in L1\n", searchValue);
}
// 在 L1 的第二个结点后插入值为 8 的新结点
p = L1->next; // 重新指向第一个结点
while (p != NULL && p->data != 2) {
p = p->next;
}
if (p != NULL) {
insert(p, 8);
printf("\nAfter inserting 8 after the second node in L1: ");
p = L1->next;
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
// 删除 L1 的第三个结点后的结点
p = L1->next; // 重新指向第一个结点
while (p != NULL && p->data != 3) {
p = p->next;
}
if (p != NULL) {
deleter(p);
printf("\nAfter deleting the node after the third node in L1: ");
p = L1->next;
while (p != NULL) {
printf("%d", p->data);
p = p->next;
}
printf("\n");
}
// 删除 L2 中值为 7 的结点
deleter1(L2, 7);
printf("\nAfter deleting nodes with value 7 in L2: ");
p = L2->next; // 跳过头结点
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
// 释放链表 L1 和 L2 的空间
p = L1;
while (p != NULL) {
LinkList temp = p;
p = p->next;
free(temp);
}
p = L2;
while (p != NULL) {
LinkList temp = p;
p = p->next;
free(temp);
}
return 0;
}
(二)静态链表
1.创建一个结构体
typedef struct {
int data;//该元素
int cur;//下一个元素
}component, SLinkList[MAXSIZE];
//数组的第0分量是头结点
2.查找元素
int LocateElem_SL(SLinkList S, int e) {
int i = S[0].cur;//i指示表中第一个结点
while (i && S[i].data != e)//在表中顺链查找
i = S[i].cur;//相当于指针后移
return i;
}
各类链表判空条件
带头点的单链表 head->next=NULL
不带头指针的单链表 head=NULL
不带头结点的循环单链表head=NULL
不带头结点的双链表:head = NULL
不带头结点的循环双链表:head = NULL
带表头结点的循环双链表:h->next = h; h->prior = h;
线性表其他操作
现在我们来对链表进行一些其他操作
1.将两个有序链表归并为一个
void MergeList_L(LinkList& La, LinkList& Lb, LinkList& Lc) {
//La,Lb非递减排序,归并得到Lc也是
LinkList pa = La->next;
LinkList pb = Lb->next;
LinkList pc = La;
Lc = La;//用La的头结点作为Lc的头结点
while (pa && pb) {
if (pa->data <= pb->data) {
pc->next = pa;
pc = pa;
pa = pa->next;
}
else {
pc->next = pb;
pc = pb;
pb = pb->next;
}
}
pc->next = pa ? pa : pb;//插入剩余段
free(Lb);//释放Lb的头结点
}
2.求两个线性表的并集
void unions(LinkList& La, LinkList Lb) {
//在Lb中且不在La中的元素插入a中
int La_len = ListLength(La);
int Lb_len = ListLength(Lb);
for (i = 1; i < Lb_leng; i++) {
GetElem(Lb, i, e);//取Lb中第i个元素赋给e
if(!LocateElem(La,e,equal))ListInsert(La,++La_len,e);//La中不存在和e相同的数据元素,则插入
}
}
总结一下线性表各种操作的时间复杂度吧
顺序结构
查找:O(1)
插入:O(n)
删除:O(n)//通过下标直接找到待操作元素,主要时间花在移动元素上
链式结构 查找:O(n) 插入:O(n)//主要时间用于找到插入元素的位置 删除:O(n)//主要时间用于找到待删除元素的位置