数据结构与算法——算法、时间空间复杂度、线性表

一.算法

1.推导大O阶方法:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.常见的时间复杂度:

在这里插入图片描述
在这里插入图片描述

3.最坏情况与平均情况

在这里插入图片描述

4.算法的空间复杂度

在这里插入图片描述
在这里插入图片描述

二.线性表

1.线性表定义:

0个多个数据元素组成的有限序列

注意:

  1. 他是一个序列
  2. 若存在多个元素,则第一个元素无前驱,最后一个元素无后继,其他元素都有且只有一个前驱和后继;
  3. 线性表强调有限的

2.线性表的顺序存储结构:

结构代码:

#include <stdio.h>
#define MAXSZIE 20;
typedef int ElemType;
typedef struct 
{
	ElemType data[MAXSIZE];
	int length;			//线性表当前的长度
}SqList; 

顺序存储结构封装需要的三个属性:

  • 存储空间的起始位置数组data,他的存储位置就是线性表存储空间的存储位置;
  • 线性表的最大存储容量:数组的长度MAXSIZE(是存放线性表的存储空间的总长度,初始化后不会变);
  • 线性表的当前长度:length(是线性表的元素个数,是会变化的)。
地址的计算方法:

1.线性表从1开始,回归正常思维;

2.在这里插入图片描述

2.1.线性表顺序存储结构获得数组元素
#define OK 1;
#define ERROR 0;
#define TRUE 1;
#define FALSE 0; 

typedef int Status;

//Status 是函数的类型,其值是函数结果状态代码,如OK等;
//初始条件:顺序线性表L已存在,1 <= i <= ListLength(L);
//操作结果:用e返回L中第i个数据元素的值。

Status GetElem (Sqlist L,int i,ElemType *e)
{
	if(L.length == 0 || i < 1 || i > L.length )
	{
		return ERROR;
	}
	*e = L.data[i-1];
	
	return OK ;
} 
2.2.线性表顺序存储结构插入操作

插入算法思路:

  1. 如果插入位置不合理,退出程序;

  2. 如果线性表长度>=数组长度,则退出程序or动态增加数组容量

  3. 最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;

  4. 将要插入元素填入位置i处;

  5. 线性表长+1。

    代码实现:

Statua Listinsert(Sqlist *L ,int i ,ElemType e)
{
	int k;
	if(L->length == MAXSIZE)		//表示线性表已经满了
	{
		return ERROR;
	}
	if(i < 1 || i > L->length + 1)	//i不在范围内 
	{
		return ERROR;
	}
	if(i <= L->length)			//若插入的不在表尾 
	{
		//将要插入位置后数据元素向后移一位 
		for(k = L->length; k >= i - 1; k--)	 
		{
			L->data[k+1] = L->data[k];
		} 
	}
	
	L->data[i-1] = e;			//将新元素插入 
	L->length++;
    
    return OK;
 } 
2.3.线性表顺序存储结构删除操作

删除算法思路:

  1. 如果删除位置不合理,退出程序;

  2. 取出删除元素;

  3. 从删除元素位置开始遍历到最后一个元素位置,分别将他们都向前移动一个位置;

  4. 表长-1。

    代码实现:

    Status ListDelete(Sqlist *L,int i,ElemType *e)
    {
    	int k;
    	if(L->length == 0)				//线性表为空 
    	{
    		return ERROR;
    	}
    	if(i < 1 || i > L->length) 		//删除位置不正确 
    	{
    		return ERROR;
    	}
    	
    	*e = L->data[i-1];			//取出删除元素
    	
    	if( i < L->length )				//如果删除不是最后位置
    	{
    		for(k = i;k < L->length;k++)//将删除位置后继元素前移
    		{
    			L->data[k-1] = L->data[k];
    		}
    		
    		L->length--;
    	}
    		return OK; 
     
    }
    
    2.4.线性表顺序存储结构的优缺点:
    • 结构在存、读数据时,时间复杂度都是O(1);

      而在插入或删除时,时间复杂度都是O(n)。

    • 这种结构比较适合元素个数比较稳定,不轻易插入和删除元素,更多的操作是存取数据的应用。

在这里插入图片描述

3.线性表静态链表:

用数组描述的链表,称之为静态链表。这种描述方法叫做游标实现法。
在这里插入图片描述
最后一个游标指向第一个有数据元素的下标

第一个游标指向第一个没数据下标

除此之外,每个元素的游标都指向下一个元素的下标

3.1静态链表存储结构:
#define MAXSIZE 1000
typedef struct{
	ElemType data;		//数据
	int cur;			//游标(Cursor) 
}Component, StaticLinkList[MAXSIZE]; 
3.2静态链表的初始化:

相当于初始化数组。

Status IntLinst(StaticLinkList space)
{
	int i;
	for(i = 0;i < MAXSIZE - 1;i++)
	{
		space[i].cur= i + 1;		//游标指向下一个元素下标 
	}
		space[MAXSIZE-1].cur = 0;		//最后一个游标指向0 
	
	return OK;
}

注意事项:

  • 数组的第一个最后一个元素做特殊处理,他们的data不存放数据
  • 通常把未使用的数组元素称为备用链表
  • 数组的第一个元素(下标为0的元素的cur),则存放备用链表的第一个结点的下标。
  • 数组的最后一个元素(下标为MAXSIZE-1的cur),则存放第一个有数值的元素的下标,相当于单链表的头结点作用。

4.循环链表

优点:从表中任一结点出发均可找到表中其他结点。
在这里插入图片描述
**注意:**1.循环链表要想表示空,不是判断p或者p->next是否为空,而是判断他们是否等于头指针

在这里插入图片描述

4.1 头尾指针表示单循环链表
4.1.1:头指针表示单循环链表在这里插入图片描述
要找的结点时间复杂度
a1O(1)
anO(n)

由此我们可以发现,头指针表示循环链表,非常不方便

注意:表的操作常常是在表的首尾位置上进行。

4.1.2:尾指针表示单循环链表
要找的结点该结点储存位置时间复杂度
a1R->next->nextO(1)
anRO(1)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RwUosqki-1638501994892)(C:\Users\1涵\AppData\Roaming\Typora\typora-user-images\image-20211130171858943.png)]
说明: R->next头结点

4.2:合并带尾指针的循环链表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e6vzTgVI-1638501994893)(C:\Users\1涵\AppData\Roaming\Typora\typora-user-images\image-20211130195516163.png)]
1.p存Ta表头结点

p = Ta->next;

2. 将Tb表头连接到Ta表尾巴
在这里插入图片描述
3.释放Tb表头结点
在这里插入图片描述
4.修改指针
在这里插入图片描述

合并小结:

在这里插入图片描述
代码实现:

LinkList Connect(LinkList Ta,Linklist Tb)//假设Ta.Tb都是非空的单循环链表 
{
	LinkList p = Ta->next;			//p保存头结点 
	Ta->next = Tb->next->next;		//Tb表头保存Ta表尾 
	free(Tb->next);						//释放Tb表头结点 
	Tb->next = p;					// 修改指针,b尾指回a头 
	
	return Tb;
}

5.双向链表

双向链表:在单链表的每个结点里面再增加一个指向其直接前驱的指针域prior,这样链表中就形成了有两个方向不同的链,故称为双向链表

5.1:双向链表的结构可定义如下:

在这里插入图片描述
代码如下:

typedef struct DulNode{
	Elemtype data;
	struct DulNode *prior,*next;
}DulNode,*DuLinkList;

5.2:双循环链表
  • 头结点的前驱指针指向链表的最后一个结点

  • 最后一个结点的后继指针指向头结点

如图:


在这里插入图片描述

5.3:双向链表结构的对称性(设指针p指向某一结点)
p->prior->next = p = p->next->prior

在这里插入图片描述
注意:双向链表中的有些操作(例如:获取链表的长度获取链表的元素等),因为仅涉及一个方向的指针,故他们的算法与线性链表的相同。但在插入删除时,则需同时修改两个方向上的指针,两者操作的时间复杂度均为O(n)

5.4:双向链表的插入

思路:
1.s->prior = p->prior;		
2.p->prior->next = s;
3.s->next = p;
4.p->prior = s;

代码如下:

void ListInsert_Dul(DuLinkList &L,int i,ElemType e)
{
//在带有头结点的双向循环链表L中第i个位置之前插入元素e 
	if(!( p = GetElemP_DUL(L,i)))		//位置不合理,得到空指针 
	{
		return ERROR;
	}
	s = newDULNode;			//找到新节点 
	s->data = e;			//赋值 
	
	s->prior = p->prior;
	p->prior->next = s;
	s->next = p;
	p->prior = s;
	
	return OK;	
}
5.5:双向链表的删除

思路:
1.p->prior->next = p->next;
2.p->next->prior = p->prior;

代码如下:

void ListDelete_DuL(DuLinklist &L,int i,ElemType &e)
{
//删除带头结点的双向链表 L的第i个元素,并用e返回
	if(!( p = GetElemP_DuL(L,i)))
	{
		return ERROR;
	}
	e = p->data;		
	
	p->prior->next = p->next;
	p->next->prior = p->prior;
	free(p);
	
	return OK; 
} 
三种链表的时间效率比较:

链式存储结构的优缺点:
链式存储结构的优点链式存储结构的缺点
节点空间可以动态申请和释放存储密度小,每个节点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大
数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的

6.顺序表和链表的比较

在这里插入图片描述

7.线性表的应用

7.1:线性表的合并

问题描述:

​ 假设利用两个线性表La和Lb分别表示两个集合A和B,先要求一个新的集合A=AUB

​ La = (7,5,3,11) Lb = (2,6,3) ----> La = (7,5,3,11,2,6)

算法步骤:

依次取出Lb中的每个元素,执行一下操作:

1.在La中查找该元素;

2.如果找不到,则将其插入La的最后。

代码实现

void whole(List &La,List Lb)
{
	La_len = ListLength(La);
	Lb_len = ListLength(Lb);
	for(i = 1;i <= Lb_Length;i++)
	{
		FetElem(Lb,i,e);
		if(!LocateElem(La,e))
		{
			ListInsert(&La,++La_lenth,e);
		} 
	}
} 

*算法的时间复杂度是:O(ListLength(La)ListLength(Lb)

7.2:有序表的合并

问题描述:

​ 已知线性表La和Lb中的数据元素按值非递减有序排列,现要求将La和Lb归并为一个新的线性表Lc,且Lc中的数据元素仍按值非递减有序排列

​ La = (1,7,8)Lb = (2,4,6,8,10,11) ----> Lc = (1,2,4,6,7,8,8,10,11)

算法步骤:

  1. 创建一个空表Lc;
  2. 依次从La或Lb中“摘取“元素值较小的结点插入到Lc表的最后,直至其中一个表变空表为止;
  3. 继续将La或Lb其中一个表的剩余结点插入在Lc表的最后。

实现方法:

  • 顺序表实现
    在这里插入图片描述
void MergeList_Sq(SqList LA,SqList LB,SqList &LC)
{
	pa = LA.elem;			
	pb = LB.elem;			//指针pa和pb的初值为别指向两个表的第一个元素 
	LC.length = LA.length + LB.length;	//新表长度为带合并两表的长度之和 
	LC.elem = new ElemType[LC.length];	//为合并后的新表分配一个数组空间 
	pc = LC.elem;						//指针pc指向新表的第一个元素 
	pa_last = LA.elem + LA.length - 1;	//指针pa_last指向LA表的最后一个元素 
	pb_last = LB.elem + LB.length - 1; 	//指针pb_last指向LB表的最后一个元素 
    while(pa <= pa_last && pb <= pa_last)	//两个表都非空
	{
		if(*pa <= *pb)			//依次"摘取"两表中较小的结点 
        {
            *pc++ = *pa++;
        }
        else
        {
            *pc++ = *pb++;
        }
    }
	while(pa <= pa_last)
	{
		*pc++ = *pa++;   //LB表已到达表尾,将LA中剩余元素加入LC 
	}
	while(pb <= pb_last)
	{
		*pc++ = *pb++;	//LA表已到达表尾,将LB中剩余元素加入LC 
	} 
} 

算法时间和空间复杂度都是:O(ListLength(La) + ListLength(Lb))

  • 用链表实现
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    以此类推…

接下来 插入剩余段:
在这里插入图片描述
合并后:
在这里插入图片描述
代码实现:

void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc)
{
	pa = La->next;
	pb = Lb->next;
	pc = 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;
		} 
	}
	if(pa)					//删除剩余段 
	{
		pc->next = pa;
	}
	else
	{
		pc->next = pb;
	}
	
	free(Lb);		//释放Lb的头结点 
} 

算法的时间复杂度:O(ListLength(La) + ListLength(Lb))

算法的空间复杂度:O(1)


声明此篇文章是自己在学习同时整理归纳的笔记,参考了B站小甲鱼和王卓老师的视频,图片也是从视频中截取了需要的部分。

王卓老师https://www.bilibili.com/video/BV1nJ411V7bd?p=48
小甲鱼https://www.bilibili.com/video/BV1jW411K7yg?p=9

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值