数据结构笔记②——线性表
数据结构
♥made by Randq♥ ^_^
数据结构
第二章 线性表
2.1 线性表的定义及基本操作
线性表的定义
线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列,其中n为表长,当n=0时线性表示一个空表。
若用L命名线性表,则其一般表示为L = (a1 , a2 , ... , ai , ai+1 , ... , an)
ai是线性表中放入“第i个”元素 (位序从1开始,数组下标从0开始)
a1是表头元素,an是表尾元素
除了第一个元素,所有元素都有唯一前驱;除了最后一个元素,所有元素都有唯一后继
线性表的基本操作
2.2 线性表的顺序表示
存储结构
顺序表——用顺序存储的方式实现线性表
$顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。$
实现方式
静态分配
//静态分配-使用“静态数组实现”
#define MAXSIZE 10
typedef struct {
ElemType data[MAXSIZE];//用静态的“数组”存放数据元素,大小一旦确定就无法改变
int length;
}SqList; //顺序表的类型定义——静态分配方式
//基本操作——初始化一个线性表
void InitList(SqList &L) {
for(int i=0; i<MAXSIZE; i++){
L.data[i] = 0;
}
L.length = 0;
}
动态分配
//动态分配-使用“动态数组实现”
#define InitSize 10 //顺序表的初始长度
typedef struct {
int * data; //指示动态分配数组的指针
int MAXSIZE; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList; //顺序表的类型定义——动态分配方式
void InitList(SeqList &L) {
L.data = (int *)malloc(InitSize*sizeof(int));
L.length = 0;
L.MAXSIZE = InitSize;
}
//增加动态数组的长度
void IncreaseSize(SeqList & L, int len) {
int * p = L.data;
L.data = (int *)malloc((MAXSIZE+len)*sizeof(int));
for(int i=0; i<L.length; i++){
L.data[i] = p[i];//将数据复制到新区域
}
L.MAXSIZE = MAXSIZE+len;//顺序表最大长度增加 len
free(p);//释放原来的空间
}
特点
- 随机访问,可以在O(1)时间内找到第i个元素。
- 存储密度高,每个结点只储存数据元素。
- 拓展不方便(即使采用动态分配的方式实现,拓展长度的时间复杂度也较高)
- 插入,删除操作不方便,需要移动大量元素
基本操作
顺序表的插入
//基本操作
//插入
bool ListInsert (SqList &L, int i, int e) {//在L的位序i处插入元素e
if( i<1 || i>length+1) return false;//判断i的位置是否有效
if(L.length>=MAXSIZE) return false;//当前空间已满不能插入
for(int j=L.length; j>=i; j--) {
L.data[j] = L.data[j-1];
}
L.data[i-1] = e;
L.length++;
} //时间复杂度:最好O(1),最坏O(n),平均O(n)
顺序表的删除
//删除
bool ListDelete (SqList & L, int i, int & e) {
if(i<1 || i>length) return false; //判断i的位置是否合法
e = L.data[i-1];
for(int j=i; j<=L.length-1; j++) {
L.data[j-1] = L.data[j];
}
L.length--;
return true;
}//时间复杂度:最好O(1),最坏O(n),平均O(n)
顺序表的查找
//查找
//按位查找
ElemType GetElem (SqList L, int i) {
return L.data[i-1];
} //时间复杂度O(1)
//按值查找;在顺序表中查找第一个元素值等于e的元素,并返回其位序
int LocateElem (SqList L, ElemType e) {
for(int i=0; i<L.length; i++){
if(L.data[i]==e)//结构体的比较不能直接使用==,需要写一个比较函数
return i+1;
}
return 0;
}//时间复杂度:最好O(1),最坏O(n),平均O(n)
2.3 线性表的链式表示
单链表的定义
单链表
用“链式存储”(存储结构)实现了“线性结构”(逻辑结构),一个结点存储一个数据元素, 各结点间的先后关系用一个指针来表示
注:代码中有些结点申请 没有考虑到没有申请到的情况
用代码定义一个单链表
typedef struct LNode {//定义单链表节点类型
ElemType data;//数据与 每个结点存放一个数据元素
struct LNode * next;//指针域 指针指向下一个结点
}LNode, *LinkList;
LNode * p = (LNode *)malloc(sizeof(struct LNode));
//要表示一个单链表时,只需要声明一个头指针L,指向单链表的第一个结点
// LNode * L;//声明一个指向单链表第一个结点的指针
// LinkList L;
// 强调这是一个单链表 —— 使用LinkList
// 强调这是一个结点 —— 使用LNode *
单链表的两种实现
带头结点
//带头结点的单链表 //初始化一个单链表 bool InitList (LinkList & L) { L = (LNode *)malloc(sizeof(LNode));//分配一个头结点 if(L==NULL)//内存不足,分配失败 return false; L->next = NULL;//头结点之后暂时还没有结点 return true; } //判断单链表是否为空 bool Empty (LinkList L) { return (L->next == NULL); } void test(){ LinkList L;//声明一个指向单链表的指针 InitList(L);//初始化一个指针 //...后续代码... }不带头结点
//不带头结点的单链表 //初始化一个单链表 bool InitList (LinkList & L) { L = NULL; return true; } //判断单链表是否为空 bool Empty (LinkList L) { return (L==NULL); } void test(){ LinkList L;//声明一个指向单链表的指针 InitList(L);//初始化一个指针 //...后续代码... }
单链表上基本操作的实现
单链表的插入
按位序插入
//在第i个位置插入元素e(带头结点) bool ListInsert (LinkList &L, int i, ElemType e) { if(i<1) return false; LNode * p = GetElem(L, i-1); return InsertNextNode(p, e); } bool ListInsert (LinkList &L, int i, ElemType e) { if(i<1) return false; LNode * p;//指针p是当前扫描到的结点 int j = 0;//当前p指向的是第几个结点 p = L;//L指向头结点,头结点是第0个结点(不存数据) while (p!=NULL && j<i-1) {//循环找到第i-1个结点 p = p->next; j++; } if(p==NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = p->next;//这两句不可颠倒... p->next = s;//将s结点连到p之后 return true; } //在第i个位置插入元素e(不带头结点) bool ListInsert (LinkList &L, int i, ElemType e) { if(i<1) return false; if(i==1) {//插入第一个结点的操作与其他结点操作不同 LNode *s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = L; L = s; return true; } LNode * p;//指针p是当前扫描到的结点 int j = 1;//当前p指向的是第几个结点 p = L;//L指向第一个结点 while (p!=NULL && j<i-1) {//循环找到第i-1个结点 p = p->next; j++; } return InsertNextNode(p, e); }指定结点的后插操作
//后插操作:在p结点之后插入元素e bool InsertNextNode (LNode *p, ElemType e) { if(p==NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s;//将s结点连到p之后 return true; }指定结点的前插操作
//前插操作:在p结点前插入元素 e bool InsertPriorNode (LNode *p, ElemType e) { if(p==NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); s->next = p->next; p->next = s;//新结点s连到p后 s->data = p->data;//将p中元素复制到s中 p->data = e;//p中元素覆盖为e return true; } //前插操作:在p结点前插入结点s bool InsertPriorNode (LNode *p, LNode *s) { if(p==NULL || s==NULL) return false; s->next = p->next; p->next = s;//新结点s连到p后 ElemType tmp = p->data;//交换数据域部分 p->data = s->data; s->data = tmp; return true; }
单链表的删除
按位序删除(带头结点)
//按位序删除(带头结点) bool ListDelete (LinkList &L, int i, ElemType &e) { if(i<1) return false; LNode *p;//指针p指向当前扫描到的结点 int j = 0;//当前p指向的是第几个结点 p = L;//指向头结点,头结点是第0个结点 while (p!=NULL || j<i-1) {//循环找到第i-1个结点 p = p->next; j++; } if(p==NULL) return false; LNode *q = p->next;//q指向被删除结点 e = q->data;//用e返回元素的值 p->next = q->next;//将*q从链中断开 free(q);//释放结点的存储空间 return true; //删除成功 }指定结点的删除(指定结点是最后一个节点时,需要特殊处理)
//指定结点的删除 bool DeleteNode (LNode *p) { if(p==NULL) return false; if(p->next != NULL){//如果p不是最后一个结点 LNode *q = p->next;//令q指向*p的后继结点 p->data = q->data;//和后继结点交换数据域 p->next = q->next;//将*q从链中断开 free(q);//释放后继结点的存储空间 } else if(p->next == NULL){ LNode *q = L; int j = 0; while (q->next->next!=NULL) { q = q->next; j++; } free(p); q->next = NULL; } return true; }
单链表的查找
按位查找
//单链表的查找 //按位查找:返回第i个元素(带头结点) LNode * GetElem(LinkList L, int i) { if(i<0) return false; LNode *p;//指针p指向当前扫描到的结点 int j = 0;//当前p指向的是第几个结点 p = L;//指向头结点,头结点是第0个结点 while (p!=NULL || j<i) {//循环找到第i个结点 p = p->next; j++; } return p; }按值查找
//按值查找 LNode * LocateElem (LinkList L, ElemType e) { LNode *p = L->next; while(p!=NULL && p->data != e){//这里假设data的值可以直接比较 p = p->next; } return p;//找到后返回该结点指针,否则返回NULL }求单链表长度
int Length(LinkList L) { int len = 0; LNode *p = L; while(p->next!=NULL) { p = p->next; len++; } return len; }
单链表的建立
尾插法与头插法:
//头插法建立单链表 //重要应用:链表的逆置 LinkList List_HeadInsert (LinkList & L) {//逆向建立单链表 LNode * s, int x; L = (LNode *)malloc(sizeof(LNode)); //创建头结点 L->next = NULL;//初始为空链表 scanf("%d", &x);//输入结点的值 while(x!=9999) {//输入9999表示结束 s = (LNode *)malloc(sizeof(LNode));//创建新结点 if(s==NULL) return false; s->data = x; s->next = L->next; L->next = s; scanf("%d", &x); } return L; }//尾插法建立单链表 LinkList List_TailInsert (LinkList & L) { int x; L = (LNode *)malloc(sizeof(LNode)); //创建头结点 LNode * s,* r = L;//r为表尾指针 L->next = NULL;//初始为空链表 scanf("%d", &x);//输入结点的值 while(x!=9999) {//输入9999表示结束 s = (LNode *)malloc(sizeof(LNode));//创建新结点 if(s==NULL) return false; s->data = x; r->next = s;//在r结点之后插入元素x r = s; //永远保持r指向最后一个结点 scanf("%d", &x); } r->next = NULL;//尾结点指针置空 return L; }
双链表
双链表的初始化
//双链表:可进可退,存储密度稍低一点
//双链表的初始化
typedef struct DNode {
ElemType data;
struct DNode *prior,*next;
}DNode, *DLinkList;
bool InitDLinkList (DLinkList L) {
L = (DNode *)malloc(sizeof(DNode));//分配一个头结点
if(L == NULL)//内存不足,分配失败
return false;
L->prior = NULL;//头结点的prior永远指向NULL
L->next = NULL;//头结点之后暂时还没有结点
return true;
}
bool Empty(DLinkList L) {c
return (L->next == NULL);
}
双链表的插入
//双链表的插入——在p结点之后插入s结点
bool InsertNextDNode(DNode *p, DNode *s){
if(p==NUll || s==NULL)//非法参数
return false;
s->next = p->next;
if(p->next != NULL) //如果p后有后继结点
p->next->prior = s;
s->prior = p;
p->next = s
return true;
}
双链表的删除
//双链表的删除
bool DeleteNextDNode(DNode *p){
if(p==NULL) return false;
DNode *q = p->next;//找到p的后继q
if(q==NULL) return false;//p没有后继
p->next = q->next;
if(q->next!=NULL)
q->next->prior = p;
free(q);
return true;
}
void DestoryList (DLinkList &L) {
//循环释放各个数据结点
while (L->next!=NULL)
DeleteNextDNode(L);
free(L);//释放头结点
L = NULL;//头指针指向NULL
}
双链表的遍历
//双链表的遍历
//后向遍历
while(p!=NULL) {
p = p->next;
}
//前向遍历
while(p!=NULL) {
p = p->prior;
}
//前向遍历(跳过头结点)
while(p->prior!=NULL) {
p = p->prior;
}
循环链表
循环单链表
单链表:表尾节点的next指针指向NULL ——从一个结点出发只能找到后续的各个结点
循环单链表:表尾的next指针指向头结点 ——从一个结点出发可以找到其他任何一个结点
从头部找到尾部 O(n),从尾部找到头部O(1),很多时候对链表的操作都是在头部或尾部,可以让R指向表尾因素(插入,删除时需要修改R)
//循环链表 //初始化一个循环单链表 bool InitList (LinkList L) { L = (LNode *)malloc(sizeof(LNode));//分配一个头结点 if(L == NULL)//内存不足,分配失败 return false; L->next = L;//头结点的next指向头结点 return true; } //判断循环单链表是否为空 bool Empty(LinkList L) { return (L->next == L); } //判断结点p是否为循环单链表的表尾结点 bool isTail(LinkList L, LNode * p) { return (p->next == L); }
循环双链表
双链表:表头结点的prior指向NULL;表尾结点的next指向NULL
循环双链表:表头结点的prior指向表尾结点;表尾结点的next指向表头结点
typedef struct DNode { ElemType data; struct DNode *prior,*next; }DNode, *DLinkList; //初始化空的循环双链表 bool InitDLinkList (DLinkList L) { L = (DNode *)malloc(sizeof(DNode));//分配一个头结点 if(L == NULL)//内存不足,分配失败 return false; L->prior = L;//头结点的prior指向头结点 L->next = L;//头结点的next指向头结点 return true; } //判断循环双链表是否为空 bool Empty(DLinkList L) { return (L->next == L); } //判断结点p是否为循环双链表的表尾节点 bool isTail(DLinkList L, DNode * p) { return (p->next == L); } //在p结点之后插入s结点 bool InsertNextDNode(DNode *p, DNode *s){ s->next = p->next; p->next->prior = s; s->prior = p; p->next = s; return true; } //删除p结点的后继节点q bool DeleteNextDNode(DNode *p){ DNode *q = p->next; p->next = q->next; q->next->prior = p; free(q); return true; }
静态链表
静态链表:分配一整片连续的内存空间,各个结点集中安置
每个数据元素4B,每个游标4B(每个结点共8B)
设起始地址为addr,e1的存放地址为addr+8*2
初始化静态链表 把a[0].next = -1;其他结点的next = -2,表示结点空闲。 查找 从头结点出发挨个往后遍历节点 插入位序为i的结点 找到一个空的结点,存入数据元素->从头结点出发找到位序为i-1的结点->修改新结点的next->修改i-1号结点的next 删除某个结点 从头结点出发找到前驱结点->修改前驱结点的游标->被删除结点的游标设为-2
顺序表和链表的比较
逻辑结构:都属于线性表,都是线性结构
存储结构:顺序存储和链式存储
基本操作:
基本操作 顺序表 链表 创 需要预分配大片连续空间,若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源。 静态分配:静态数组(容量不可改变)动态分配:动态数组(容量可改变,但需要移动大量元素,时间代价高) 只需要分配一个头结点,方便拓展 销 修改length = 0 静态分配:静态数组(系统自动回收空间)动态分配:动态数组(需要手动free) 依次删除各个结点(free) 增删 插入、删除元素都要将后续元素都前移,后移;时间复杂度O(n),时间开销用于移动元素 插入、删除元素只需修改指针即可;时间复杂度O(n),时间开销用于查找目标元素。 查 按位查找:O(1) 按值查找:O(n),若表内元素有序,可在O(lgn)时间内找到 按位查找:O(n) 按值查找:O(n) 使用:当表长难以估计,需要增加/删除元素时 ——链表
表长可预估,查询(搜索)操作较多 ——顺序表

浙公网安备 33010602011771号