数据结构-线性结构
线性表
线性表是最简单最常见的数据结构,属于逻辑结构;
线性表有两种实现方式(存储方式),分别是顺序实现和链接实现;
定义:
线性表是由n(>=0)个数据元素组成的有限序列
,数据元素的个数n定义为表的长度;
术语:
- 前驱, 后继, 直接前驱, 直接后继, 长度, 空表
案例:
线性表用L表示,一个非空线性表可记为L = (a1,a2,..an);
a1后面的称为a1的后继
an前面的称为an的前驱
a1为起始节点,an为终端节点,任意相邻的两个元素,如a1和a2,a1是a2的直接前驱,a2是a1的直接后继;
线性表中元素个数即表的长度,此处为n;
表中没有任何元素时,称为空表
除了首节点和尾节点之外,每个节点都有且只有一个直接前驱和直接后继,首节点没有前驱,尾节点没有后继;
节点之间的关系属于一对一;
线性表的基本运算
- 初始化
Initiate(L) 建立一个空表L(),L不包含数据元素
- 求表长度
Length(L) 返回线性表的长度
- 取表元素
Get(L,i) 返回线性表的第i个元素,i不满足1<=i<=Length(L)时,返回特殊值;
- 定位
Locate(L,x)查找x在L中的节点序号,若有多个匹配的返回第一个,若没有匹配的返回0;
- 插入
Insert(L,x,i)将x插入到L的第i个元素的前面(其他元素往后挪),参数i取值范围为1<=i<=Length(L)+1;运算结束后表长度+1;
- 删除
Delete(L,i)删除表L中的第i个元素,i有效范围1<=i<=Length(L);操作结束后表长度-1
强调:上述的第i个指的是元素的序号从1开始,而不是下标从0开始;
另外:插入操作要保证操作后数据还是一个接着一个的不能出现空缺;
线性表的顺序存储实现
线性表是一种逻辑结构,可以通过顺序存储结构来实现,即:
将表中的节点一次存放在计算机内存中一组连续的存储单元中,数据元素在线性表中的邻接关系决定了它们在存储空间中的存储位置;换句话说逻辑结构中相邻的两个节点的实际存储位置也相邻;
用顺序存储结构实现的线性表也称之为为顺序表,一般采用数组来实现;
图示:
大小与长度:
线性表的大小:指的是最大能存储的元素个数
线性表的长度:指的是当前已存储的个数
示例:
c语言实现:
#include <stdio.h>
//初始化操作:
const MAX_SIZE = 5;//最大长度
typedef struct list {
int data[MAX_SIZE];//数组
int length;//当前数据长度
};
//获取targert在表中的位置
int locate(struct list *l,int target){
for (int i = 0;i < l->length;i++){
if (target == l->data[i]){
return i + 1;
}
}
return 0;
}
//获取第loc个元素
int get(struct list *l,int loc){
if (loc < 1 || loc > l->length){
printf("error:位置超出范围\n");
return -1;
}else{
return l->data[loc-1];
}
}
//插入一个元素到第loc个位置上
void insert(struct list *l,int data,int location){
if (l->length == MAX_SIZE){
printf("errolr:表容量已满\n");
return;
}
if (location < 1 || location > l->length+1){
printf("error:位置超出范围\n");
return;
}
//目标位置后面的内容以此往后挪
for (int i = l->length; i >= location; i--) {
l->data[i] = l->data[i-1];
}
//在目标位置放入新的数据
l->data[location-1] = data;
l->length+=1;//长度加1
}
//删除第loc个元素,从目标位置往后的元素一次向前移动
void delete(struct list *l,int loc){
if (loc < 1|| loc > l->length){
printf("error:位置超出范围\n");
return;
}
//目标位置及后面的所有元素全部向后移动
for (;loc < l->length; ++loc) {
l->data[loc-1] = l->data[loc];
}
l->length-=1;
}
//打印所有元素 测试用
void show(struct list l){
for (int i = 0; i < l.length; ++i) {
printf("%d\n",l.data[i]);
}
}
//测试
int main() {
struct list alist = {};
insert(&alist,100,alist.length+1);
insert(&alist,200,alist.length+1);
insert(&alist,300,alist.length+1);
insert(&alist,400,alist.length+1);
delete(&alist,1);
printf("%d\n",alist.length);
show(alist);
printf("%d\n",get(&alist,4));
printf("%d\n", locate(&alist,300));
printf("%d\n", get(&alist,1));
return 0;
}
插入算法分析:
假设线性表中含有n个元素,
在插入元素时,有n+1个位置可以插入,因为要保证数据是连续的
每个位置插入数据的概率是: 1/(n+1)
在i的位置插入时,要移动的元素个数为:n - i + 1
算法时间复杂度为:O(n)
删除算法分析:
假设线性表中含有n个元素,
在删除元素时,有n个位置可以删除
每个位置插入数据的概率是: 1/n
在i的位置删除时,要移动的元素个数为:n - i
算法时间复杂度为:O(n)
插入与删除的不足
顺序表在进行插入和删除操作时,平均要移动大约一半的数据元素,当存储的数据量非常大的时候,这一点需要特别注意;
简单的说,顺序表在插入和删除时的效率是不够好的;特别在数据量大的情况下;
顺序表总结:
1.顺序表是一维数组实现的线性表
2.逻辑上相邻的元素,在存储结构中也是相邻的
3.顺序表可实现随机读取
优缺点:
优点:
- 无需为了表示元素直接的逻辑关系而增加额外的存储空间
- 可方便的随机存取表中的任一节点
缺点:
- 插入和删除运算不方便,需要移动大量的节点
- 顺序表要求占用连续的存储空间,必须预先分配内存,因此当表中长度变化较大时,难以确定合适的存储空间大小;
顺序表节点存储地址计算:
设第i个节点的存储地址为x
设顺序表起始地址为loc,每个数据元素占L个存储单位
计算公式为:x = loc + L * (i-1)
如 loc = 100 i = 5 L = 4 则 x = 116
线性表的链接存储实现
线性表也可通过链接存储方式来实现,用链接存储方式实现的线性表也称为链表 Link List
链式存储结构:
1.可用任意一组存储单元来存储数据
2.链表中节点的逻辑次序与物理次序不一定相同
3.每个节点必须存储其后继节点的地址信息(指针)
图示:
单链表
单链表指的是只能沿一个方向查找数据的链表,如上图
每个节点由两个部分(也称为域)组成
- data域 存放节点值得数据域
- next域 存放节点的直接后继的地址的指针域(也称为链域)
节点结构:
每个节点只知道自己后面一个节点却不知道自己前面的节点所以称为单链表
图示:
带有head节点的单链表:
单链表的第一个节点通常不存储数据,称为头指针,使用头指针来存储该节点的地址信息,之所以这么设计是为了方便运算;
单链表特点:
- 其实节点也称为首节点,没有前驱,所以头指针要指向该节点,以保证能够访问到起始节点;
- 头指针可以唯一确定一个链表,单链表可以使用头指针的名称来命名;
- 终端节点也称尾节点,没有后继节点,所以终端节点的next域为NULL;
- 除头结点之外的几点称为表结点
- 为方便运算,头结点中不存储数据
单链表数据结构定义
//数据结构定义
typedef struct node {
struct node *next;
int data,length;
} Node, *LinkList;
/*
* typedef 是用来取别名的
* Node 是struct node 的别名
* *LinkList 是 struct node *的别名
* 后续使用就不用在写struct关键字了
*/
运算:
初始化
一个空链表有一个头指针和一个头结点构成
假设已定义指针变量L,使L指向一个头结点,并使头结点的next为NULL
//时间复杂度 :O(1)
LinkList initialLinkList() {
// 定义链表的头结点
LinkList head;
//申请空间
head = malloc(sizeof(struct node));
//使头结点指向NULL
head->next = NULL;
return head;
}
求表长
从头指针开始遍历每个节点知道某个节点next为NULL为止,next不为空则个数len+1;
//求表长 时间复杂度 :O(n)
int length(LinkList list){
int len = 0;
Node *c = list->next;
while(c != NULL){
len+=1;
c = c->next;
}
return len;
}
读表元素
给定一个位置n,获取该位置的节点
遍历链表,过程中若某节点next为NULL或已遍历个数index=n则结束循环
//从链表中获取第position个位置的节点 时间复杂度 :O(n)
Node *get(LinkList list, int position) {
Node *current;
int index = 1;
current = list->next;
//如果下面还有值并且还没有到达指定的位置就继续遍历 要和查找元素区别开 这就是一直往后遍历直到位置匹配就行了
while (current != NULL && index < position) {
current = current->next;
index += 1;
}
if (index == position) {
return current;
}
return NULL;
}
定位
对给定表元素的值,找出这个元素的位置
遍历链表,若某节点数据域与要查找的元素data相等则返回当前遍历的次数index
//求表head中第一个值等于x的结点的序号(从1开始),若不存在这种结点,返回结果为0 时间复杂度 :O(n)
int locate(LinkList list,int data){
int index = 1;
Node *c;
c = list->next;
while (c != NULL){
if (c->data == data){
return index;
}
index+=1;
c = c->next;
}
return 0;
}
插入
在表的第i个数据元素结点之前插入一个以x为值的新结点new
获取第i的节点的直接前驱节点pre(若存在),使new.next = pre.next;pre.next = new;
//在表head的第i个数据元素结点之前插入一个以x为值的新结点 时间复杂度 :O(n)
void insert(LinkList list, int position, int data) {
Node *pre, *new;
if (position == 1) {
//若插入位置为1 则表示要插入到表的最前面 即head的后面
pre = list;
} else {
//pre表示目标位置的前一个元素 所以-1
pre = get(list, position - 1);
if (pre == NULL) {
printf("error:插入的位置超出范围");
exit(0);
}
}
new = malloc(sizeof(Node));
new->data = data;
new->next = pre->next;
pre->next = new;
list->length += 1;
}
删除
删除给定位置的节点
获取目标节点target的直接前驱节点pre(若pre与目标都有效),pre.next = target.next; free(target);
//删除链表中第position个位置的节点 时间复杂度 :O(n)
void delete(LinkList list,int position){
//获取要删除节点的直接前驱
Node *pre;
if (position == 1){ //如要删除的节点是第一个那直接前驱就是头结点
pre = list;
}else{
pre = get(list,position-1);
}
////如果目标和前驱都存在则执行删除
if (pre != NULL && pre->next != NULL){
Node *target = pre->next; //要删除的目标节点
//直接前驱的next指向目标的直接后继的next
pre->next = target->next;
free(target);
printf("info: %d被删除\n",target->data);
list->length -= 1;
}else{
printf("error:删除的位置不正确!");
exit(1);
}
}
创建具备指定数据节点的链表
//效率比较差算法 时间复杂度 :O(n^2)
LinkList createLinkList1(){
LinkList list = initialLinkList();
int a;//输入的数据
int index = 1; //记录当前位置
scanf("%d",&a);
while (a != -1){ // O(n)
insert(list,index++,a); // O(n^2) 每次都要从头遍历链表
scanf("%d",&a);
}
return list;
}
//尾插算法 记录尾节点 从而避免遍历 时间复杂度 :O(n)
LinkList createLinkList2(){
LinkList list = initialLinkList();
int a;//输入的数据
Node *tail = list;//当前的尾部节点
scanf("%d",&a);
while (a != -1){ // O(n)
Node * newNode = malloc(sizeof(Node)); //新节点
newNode->next = NULL;
newNode->data = a;
tail->next = newNode;//尾部节点的next指向新节点
tail = newNode;//新节点作为尾部节点
scanf("%d",&a);
}
return list;
}
//头插算法 每次插到head的后面,不用遍历但是顺序与插入时相反 时间复杂度 :O(n)
LinkList createLinkList3(){
LinkList list = initialLinkList();
int a;//输入的数据
Node * head = list;
scanf("%d",&a);
while (a != -1){ // O(n)
Node * newNode = malloc(sizeof(Node)); //新节点
newNode->next = NULL;
newNode->data = a;
newNode->next = head->next;//将原本head的next 交给新节点;
head->next = newNode;//在把新节点作为head的next;
scanf("%d",&a);
}
return list;
}
优缺点
优点:
- 在非终端节点插入删除时无需移动其他元素
- 无需预分配空间,大小没有限制(内存够的情况)
缺点:
- 无法随机存取
- 读取数据慢
链表与顺序表的对比:
操作 | 顺序表 | 链表 |
---|---|---|
读表元 | O(1) | O(n) |
定位 | O(n) | O(n) |
插入 | O(n) | O(n) |
删除 | O(n) | O(n) |