一、线性表的定义和基本操作
- 定义
线性表:具有相同类型的n(n>=0)个元素的有限序列,其中n为表长,当n=0时,该表为空表
若L命名为线性表,则一般表示为:L=(a1,a2,…,ai+1,…,an) - 特点
- 1、表中元素个数有限 ;
- 2、表中元素具有逻辑上的顺序性,在序列中各个元素排序有其先后次序
- 3、表中元素都是数据元素,每个元素都是单个元素
- 4、表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间
- 5、表中元素具有抽象性,即讨论元素间一对一的逻辑关系,而不考虑元素表示的内容
- 6、线性表是一种逻辑结构,表示元素之间一对一相邻的关系
- 线性表的九种基本操作
二、线性表的顺序表示
1、顺序表的定义
-
顺序表:线性表的顺序存储,一组地址连续存放的存储单元一次存放线性表的元素,从而使得逻辑上相邻的元素在物理位置上也相邻。逻辑顺序与物理顺序相同
-
顺序表的两种实现方法
-
数组静态分配
#define MaxSize 100 typedef struct{ ElemType data[MaxSize]; int length; }SqList;
-
数组动态分配
#define MaxSize 100 typedef struct{ ElemType *data; int length; }SqList;
-
-
动态申请空间:
C L.data = (Elemtype*)malloc(sizeof(ElemType)*InitSize); C++ L.data = new ElemType[InitSize];
L 是 SqList 类型的一个变量,也就是 L 代表这一个顺序表,接着用malloc 这个动态函数来申请空间,函数参数部分是申请空间的大小,是用 sizeof 计算每一个数据类型的大小乘以它的个数,就计算出整个需要申请空间的大小,malloc 前面的括号部分可以理解为强调了申请空间的类型。这是 C 语言中的方法。C++ 中直接 new 一个申请空间的类型和大小。
-
在使用动态分配时,一定要先申请空间才能使用,因为如果没有申请空间,它仅仅是一块地址,而没用所需要的空间。
-
静态分配和动态分配有什么不同?
其实就是数组的不同。在静态分配时,我们在编写的时候,就已经确定了数组的大小。而动态分配时,没有确定它的大小,是根据动态分配语句在运行时才将它的大小进行分配。这样有一点的好处就是,在静态分配时,当我想要存放顺序表的数据元素过超过 100 的时候则会产生错误溢出,而动态分配时,如果一旦超过了分配的空间大小,可以再重新分配一块内存空间,把旧的空间和所增加的数据元素转移到新申请的空间上,这样就不会产生溢出的问题了。这是动态分配的一个优点。 -
动态分配依旧是一块连续的存储空间,绝非是链式存储。
2、顺序表的基本操作
- 插入操作
bool ListInsert(SqList &L, int i, ElemType e) { if(i<1 || i>L.length+1) return false; 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++; return true; }
-
它的返回值类型是一个布尔类型,表示如果插入成功会返回一个 True ,插入失败会返回一个 False。它有三个参数,分别是一个引用类型的顺序表 L,这里为什么用引用类型?因为在函数体内部进行操作,其实是作用一个局部变量上的,不会真正作用到这个参数上,如果不用引用类型,是不能把插入操作真正作用到传入的顺序表上的。接着是一个整型变量 i,它表示的是插入的位置,在采用插入操作时,往往是前插法,这是一个默认规定。i 对应的是顺序表的表号而非数组的下标。最后一个参数是所插入的数据元素 e。
-
循环语句中申请了变量 j 初始化为顺序表的长度,j 进行减一操作,一直到 j=i 的时候中止循环。循环体中 L.data[j] = L.data[j-1] 的意思就是把每一个数据元素向后移了一位,一直移动到 ai 移动到原先 ai+1 的位置。 执行完了所有的循环,空出了一个位置用于插入,L.data[i-1] = e 就是把要插入的元素放到该位置。在 i 的位置,元素对应的下标是 i-1。最后将顺序表的长度加一。
-
时间复杂度
- 最好:O(1)
- 平均:O(n)
- 最坏:O(n)
-
- 删除操作
bool ListDelete(SqList &L, int i, ElemType &e) { if(i<1 || i>L.length) return false; e = L.data[i-1] for(int j=i; j<L.length; j++) L.data[j-1] = L.data[j]; L.length--; return true; }
-
函数的返回值依旧是一个布尔类型。它有三个参数,分别是一个引用类型的顺序表 L,一个整型变量 i 表示删除的位置,一个引用类型的数据元素 e ,这里使用引用类型也是因为在函数体内部进行操作,是作用在一个局部变量上的,不会真正作用到这个参数 e 上,所以说使用引用类型来将所删除的这个数据元素真正的返回到函数外部。
-
第一个条件是判断删除的位置是否合法。接着 e = L.data[i-1] 是在保存要删除的数据元素,将 i 位置的数据元素,对应下标 i-1,赋值给 e 。然后循环从 i 开始,每次进行加一操作,一直循环到 L.length-1 为止,也就是从 i 一直循环到 n-1,将 ai 赋值给 ai+1 ,也就是将数组下标为 i 的赋值给 i-1 的,这样就将 ai 之后的所有数据元素都向前移了一位。接着将顺序表的长度减一。
-
时间复杂度
- 最好:O(1)
- 平均:O(n)
- 最坏:O(n)
-
- 按值查找
int Locate(SqList &L, ElemType e) { int i; for(i=0; i<L.length; i++) if(L.data[i] == e) return i+1; return 0; }
-
这个函数的返回值是一个整型变量,它表示返回的是一个顺序表的下标,所以返回值为i+1。两个参数分别是查找的顺序表以及查找的值。所有对输入参数进行修改的操作,都使用引用类型。所有进行查找的操作都不会使用引用类型。
-
首先是一个整型变量 i ,它用来存放最终的输出结果。接着是一个循环,变量 i 从下标 0,也就是第一个元素开始,每一次循环进行加一操作,一直循环到 L.length-1 ,也就是下标 n-1 的位置,一共执行了 n 次。循环体的内容是一个判断语句,判断此时数据元素是否为要找的那个元素,如果相等返回当前顺序表的下标,也就是 i+1。最后 return 0 表示循环结束了,还没有找到对应的这个值的位置,那么就说明顺序表当中是没有该值的,此时就返回一个 return 0 表示查找失败。
-
时间复杂度
- 最好:O(1)
- 平均:O(n)
- 最坏:O(n)
-
三、线性表的链式表示
1、单链表的定义
- 线性表的链式存储又称为单链表,通过一组任意的存储单元来存储线性表中的数据元素,通过指针实现线性逻辑关系
- 单链表的实现
在程序设计语言上,通过一个结构体来实现这样一个节点,它的名字叫做 LNode ,其中包含保存数据元素的变量 data 和存放下一个数据元素的指针 LNode *next 。它这个结构体的名字不仅仅叫做 LNode,还叫做 LinkList。通过名字可以发现一个节点也可以表示为一个单链表。因为只要给出第一个节点的地址,就可以依次找到属于这个单链表的所有节点。所以通过一个指针,还可以表示该单链表。typedef struct LNode { ElemType data; struct LNode *next; } LNode, *LinkList;
- 节点
数据加地址的组合叫做单链表的一个节点 - 单链表的缺点
单链表在存放数据元素的同时,还要存放指向下一个节点位置的指针域,那么这些指针域会造成空间的浪费。单链表在存放数据元素时,没有像顺序表那样存储空间连续相邻这样一个特点,所以不能实现随机存取,只能实现顺序存取。也就是说,想要找到其中的一个数据元素,只能从第一个数据元素的位置依次查找,遍历单链表。这就是单链表的具体实现方式。 - 使用头结点的优点
- 1、链表的第一个位置和其他位置的操作统一
- 2、空表和非空表的操作统一
2、单链表的基本操作
-
头插法建立
LinkList List_HeadInsert(LinkList &L){ LNode *s; int x; L=(LinkList)malloc(sizeof(LNode));//给L申请空间 L->next=NULL; scanf("%d",&x); while(x!=9999){ s=(LNode*)malloc(sizeof(LNode)); s->data=x; s->next=L->next; L->next=s; scanf("%d",&x); } return L; }
时间复杂度:O(n)
-
尾插法建立
LinkList List_TailInsert(LinkList &L){ int x; L=(LinkList)malloc(sizeof(LNode)); LNode *s,*r=L;//s为当前要插入的节点指针,r为尾节点指针,初始化为头节点 scanf("%d",&x); while(x!=9999){ s=(LNode)malloc(sizeof(LNode));//初始化节点s s->data=x; r->next=s; r=s;//将最后一个节点指向所插入节点 scanf("%d",&x); } r->next=NULL; return L; }
时间复杂度:O(n)
-
按序号查找
LNode *GetElem(LinkList L,int i){ int j=1;//表示当前节点的序号,从1开始 LNode *p=L->next;//p指针指明了当前所查找的节点,初始化为头节点的下一个节点,因为头节点不保存数据元素 if(i==0)//输入序号是否合法,序号为0,代表输入的是头节点,返回L return L; if(i<1)//不合法,返回空表示查找失败 return NULL; while(p&&j<i){ p=p->next; j++; } return p;//找到该节点返回p }
时间复杂度:O(n)
-
按值查找
LNode *LocateElem(LinkList L,ElemType e){ LNode *p=L->next; while(p!=NULL&&p->data!=e){ p=p->next;//指向下一个节点 } return p; }
时间复杂度:O(n)
-
插入节点
- (前插法)第i号节点处插入O(n)
p=GetElem(L,i-1); s->next=p->next; p->next=s;
- (后插法)第i+1号节点处插入O(1)
直接在第i号元素的位置后插入。可以利用后插法实现数据的插入,然后将插入的元素与第i号元素交换位置,就实现了前插法
- (前插法)第i号节点处插入O(n)
-
删除节点
p=GetElem(L,i-1); q=p->next;//引用q指针,保存要删除的节点 p->next=q->next; free(q);//完成任务后释放该指针
-
求表长
int count=0; p=head; while(p->next!=NULL){ count++; p=p->next; }
时间复杂度:O(n)
3、几种常用的链表
- 3.1、双链表
- | prior | data | next |
前驱节点|数据域|后继节点typedef struct DNode{ ElemType data;//数据域 struct DNode *prior;//前驱节点 struct DNode *next;//后继节点 }DNode,*DLinklist
- 插入操作
时间复杂度:O(1)s-next=p-next; p-next->prior=s; s->prior=p; p->next=s;
- 删除操作
时间复杂度:O(1)p->next=q->next; q->next->prior=p; free(q);
- | prior | data | next |
- 3.2、循环链表
- 循环单链表:最后一个节点指针指向头节点,所有的指针形成一个环,仅设尾指针操作效率会更高
- 循环双链表:将终端结点的next指针指向链表中第一个节点,将链表中第一个结点的prior指针指向终端节点。
- 空表判断
- 循环单链表
L->next==L;
- 循环双链表
L->next==L; L->prior==L;
- 循环单链表
- 3.3、静态链表
用数组实现链式存储结构,next域存放的是下一个元素的数组下标,最后一个元素的数组下标为-1#define MaxSize 50 typedef struct DNode{ ElemType data; int next; }SLinkList[MaxSize];
四 顺序表VS链表
1 存取方式
单链表中只能实现顺序存取
顺序表中用LOC(A)+(n-1)*sizeof(ElemType)可以实现顺序存取和随机存取
2 逻辑结构和物理结构
单链表逻辑相邻物理上不一定相邻,通过指针表示逻辑关系
顺序表上逻辑相邻物理上也相邻,通过相邻表示逻辑关系
3 基本操作
- 插入&&删除
单链表为O(1)(节点指针已知);O(n)(节点指针未知),但操作时需要修改指针
顺序表为O(n)且需要大量移动元素 - 查找
按值查找中单链表和顺序表都为O(n);按序查找中单链表为O(n),顺序表为O(1)
4 内存空间
- 顺序存储:无论静态分配还是非静态分配都要预先分配合适的内存空间。(静态分配时预分配空间太大会造成浪费,太小会造成溢出;动态分配虽然不会溢出但是扩充需要大量移动元素,操作效率低)
- 链式存储:在需要时分配节点空间即可,高效方便,但指针要使用额外空间
5 三个常用操作
- 最值
- 顺序表
时间复杂度:O(n)int min=L[0]; int max=L[0]; for(int i=0;i<n;i++){ if(min>L[0]) min=L[0]; if(max<L[0]) max=L[0]; }
- 链表
时间复杂度:O(n)int min=p->next->data; int max=p->next->data; for(;p!=NULL;p=p->next){ if(min>p->data) min=p->data; if(max<p->data) max=p->data; }
- 顺序表
- 逆置
- 顺序表
时间复杂度:O(n)int i=0; int j=n-1; while(i<j){ temp=L[i]; L[i]=L[j]; L[j]=temp; }
- 链表
时间复杂度:O(n)while(p->next!=r){ temp=p->next; p->next=temp->next; temp->next=r->next; r->next=temp; }
- 顺序表
- 归并
- 顺序表
时间复杂度:O(n)int i=0,j=0; for(int k=0;i<L1_Size&&j<L2_Size;k++){ if(L1[i]<L2[j]) L[k]=L1[i++]; else L[k]=L2[j++]; } while(i<L1_Size) L[k++]=L1[i++]; while(j<L2_Size) L[k++]=L2[j++];
- 链表
时间复杂度:O(n)while(p->next!=NULL&&q->next!=NULL){ if(p->next->data<q->next->data){ r->next=p->next; p->next=p->next->next; r=r->next; }else{ r->next=q->next; q->next=q->next->next; r=r->next; } } if(p->next!=NULL) r->next=p->next; if(q->next!=NULL) r->next=q->next; free(p);free(q);
- 顺序表