数据结构基本概念

数据结构基本概念

关于算法效率的例子计算多项式的值

写程序计算给定多项式在给定点x处的值

法一:image-20221124120119024

double f( int n, double a[], double x )
{/* 计算阶数为n,系数为a[0]...a[n]的多项式在x点的值 */
	int i;
	double p = a[0];
	for ( i=1; i<=n; i++ )
		p += a[i] * pow(x, i);
	return p;
}

法二:image-20221124120150453

double f( int n, double a[], double x )
{/* 计算阶数为n,系数为a[0]...a[n]的多项式在x点的值 */
	int i;
	double p = a[n];
	for ( i=n; i>0; i-- )
		p = a[i-1] + x * p;
	return p;
}

法一相比于法二慢很多

验证一下两种方法运行的时间

介绍clock函数的用法可以计算函数运行的时间。

clock():捕捉从程序开始运行到clock()被调用时所耗费的时间。这个
时间单位是clock tick,即“时钟打点”。

常数CLK_TCK(或CLOCKS_PER_SEC):机器时钟每秒所走的时钟打点数。

#include <stdio.h>
#include <time.h>

clock_t  start, stop; /* clock_t是clock()函数返回的变量类型 */
double  duration;  /* 记录被测函数运行时间,以秒为单位 */

int main ()
{	/* 不在测试范围内的准备工作写在clock()调用之前*/

	start = clock(); /* 开始计时 */
	MyFunction(); 	 /* 把被测函数加在这里,使用时这个函数必须被替换 */
	stop = clock();	 /* 停止计时 */
	duration = ((double)(stop - start))/CLK_TCK; /* 计算运行时间 (CLK_TCK是一个常数)*/
	/* 注意CLK_TCK是机器时钟每秒所走的时钟打点数, */
	/* 在某些IDE下也可能叫CLOCKS_PER_SEC。         */

    /* 其他不在测试范围的处理写在后面,例如输出duration的值 */
	return 0;
}

比较时间的代码如下

/* 给定9阶多项式 f(x)=1*x+2*(x^2)+...+9*(x^9) */
/* 用不同方法计算f(1.1)并且比较运行时间   */                   
#include <stdio.h>
#include <time.h>
#include <math.h>

clock_t start, stop;
double duration;
#define MAXN 10  /* 多项式最大项数,即多项式阶数+1 */
#define MAXK 1e7 /* 被测函数最大重复调用次数*/
/*函数只运行一次可能时间太快,达不到1个时间单位,所以要多运行几次*/
double f1( int n, double a[], double x )
{
	int i;
	double p = a[0];
	for ( i=1; i<=n; i++ )
		p += (a[i] * pow(x, i));
	return p;
}

double f2( int n, double a[], double x )
{
	int i;
	double p = a[n];
	for ( i=n; i>0; i-- )
		p = a[i-1] + x*p;
	return p;
}

void run( double (*f)( int, double*, double ), double a[], int case_n )
{/* 此函数用于测试被测函数(*f)的运行时间,并且根据case_n输出相应的结果 */
 /* case_n是输出的函数编号:1代表函数f1;2代表函数f2                   */
	int i;

	start = clock();
	for ( i=0; i<MAXK; i++ ) /* 重复调用函数以获得充分多的时钟打点数*/
		(*f)(MAXN-1, a, 1.1);
	stop = clock();

	duration = ((double)(stop - start))/CLK_TCK;
	printf("ticks%d = %f\n", case_n, (double)(stop - start));
	printf("duration%d = %6.2e\n", case_n, duration);
}

int main ()
{
	int i;
	double a[MAXN]; /* 存储多项式的系数*/

	/* 为本题的多项式系数赋值,即a[i]=i */
	for ( i=0; i<MAXN; i++ ) a[i] = (double)i;

	run(f1, a, 1);
	run(f2, a, 2);

	return 0;
}
image-20221124132611414

很明显的看出,两个函数的运行时间上差了一个数量级。

数据结构的最终定义

  • 数据对象在计算机中的组织方式
    • 逻辑结构
    • 物理存储结构
  • 数据对象必定与一系列加在其上的操作相关联
  • 完成这些操作所用的方法就是算法

抽象数据类型

  • 数据类型

    • 数据对象集
    • 数据集合相关联的操作集
  • 抽象:描述数据类型的方法不依赖于具体实现

    • 与存放数据的机器无关
    • 与数据存储的物理结构无关
    • 与实现操作的算法和编程语言均无关

只描述数据对象集和相关操作集“是什么”,并不涉及“如何做到”的问题。

“矩阵”的抽象数据类型定义

类型名称:矩阵(Matrix)

数据对象集:一个M × \times ×N的矩阵AMxN = (aij) (i=1, …, M; j=1, …, N )由M × \times ×N个三元组< a, i, j >构成,其中a是矩阵元素的值,i是元素所在的行号,j是元素所在的列号。

操作集:对于任意矩阵A、B、C ∈ \in Matrix,以及整数i、j、M、N

  • Matrix Create( int M, int N ):返回一个M × \times ×N的空矩阵;
  • int GetMaxRow( Matrix A ):返回矩阵A的总行数;
  • int GetMaxCol( Matrix A ):返回矩阵A的总列数;
  • ElementType GetEntry( Matrix A, int i, int j ):返回矩阵A的第i行、第j列的元素;
  • Matrix Add( Matrix A, Matrix B ):如果A和B的行、列数一致,则返回矩阵C=A+B ,否则返回错误标志;
  • Matrix Multiply( Matrix A, Matrix B ):如果A的列数等于B的行数,则返回矩阵C=AB否则返回错误标志;

算法

定义

  • 一个有限指令集
  • 接受一些输入(有些情况下不需要输入)
  • 产生输出
  • 一定在有限步骤之后终止
  • 每一条指令必须
    • 有充分明确的目标,不可以有歧义
    • 计算机能处理的范围之内
    • 描述应不依赖于任何一种计算机语言以及具体的实现手段

例1 选择排序算法的伪码描述

void SelectionSort ( int List[], int N )
{ /* 将N个整数List[0]...List[N-1]进行非递减排序 */
	int i;
	for ( i=0; i<N; i++ ) {
	    /* 从List[i]到List[N-1]中找最小元,并将其位置赋给MinPosition */
        MinPosition = ScanForMin( List, i, N-1 ); 

		/* 将未排序部分的最小元换到有序部分的最后位置 */
		Swap( List[i], List[MinPosition] );
	}
}

抽象 ——

  • List到底是数组还是链表(虽然看上去很像数组)?
  • Swap用函数还是用宏去实现?

算法效率

  1. 空间复杂度S(n)
    1. 根据算法写成的程序在执行时占用存储空间的长度
  2. 时间复杂度 T(n)
    1. 根据算法写成的程序在执行时耗费的时间的长度

举例

void PrintN ( int N )
{/* 打印从1到N的全部正整数 */
	if ( N > 0 ){
		PrintN( N-1 ); 
		printf("%d\n", N );
	}
} 递归打印

image-20221124170116698

通过图我们可以看到它的内存里面占用的空间数量,与我们原始的N的大小成正比的

空间复杂度表示为S(N)=C × \times ×N,当N非常大的时候,你的空间就爆掉了,就退出了。

空间复杂度分析fun1&fun2

在方法1中

​ for ( i=1; i<=n; i++ ) (1+2+…+n)
​ p += a[i] * pow(x, i); (n2+n)/2次乘法

​ 所以T(n)=C1n2+C2n

在方法二中

​ for ( i=n; i>0; i-- )
​ p = a[i-1] + x * p; n次乘法

​ T(n)=C × \times ×n

在分析一般算法的效率时,我们经常关注下面两种复杂度

  • 最坏情况复杂度Tworst(n)
  • 平均复杂度Tavg(n)

​ Tavg(n) ≤ \leq Tworst(n)

复杂度的渐进表示法

  • T(n)=O(f(n))表示存在常数C>0,n0>0,使得当n ≥ \geq n0时有T(n) ≤ \leq C × \times ×f(n),即O(f(n))表示f(n)是T(n)的某种上界
  • T(n)=Ω(g(n))表示存在常数C>0,n0>0,使得当n ≥ \geq n0时有T(n) ≥ \geq C × \times ×f(n),即O(f(n))表示f(n)是T(n)的某种下界
  • T(n)=θ(h(n)) 表示同时有T(n)=O(h(n))和T(n)=Ω(h(n)),即θ(h(n))即是上界也是下界

复杂度分析小窍门

若有两段算法分别有复杂度T1(n) = O(f1(n))和T2(n) = O(f2(n)),则

  • T1(n)+T2(n) = max(O(f1(n)),O(f2(n))) 将两个算法相加拼在一起时,他们的上界是两个上界中大的那个
  • T1(n) × \times ×T2(n) =O(f1(n) × \times ×f2(n)) 将两个算法嵌套起来时,两个复杂度相乘,他们的上界就是他们上界的乘积

若T(n)是关于n的k阶多项式,那么T(n)=θ(nk)

一个for循环的时间复杂度等于循环次数乘以循环体代码的复杂度

if-else结构的复杂度取决于if的条件判断复杂度和两个分支部分的复杂度,总体复杂度取三者中最大

应用实例

最大子列和问题

给定N个整数的序列{A1,A2, …,An},求函数f(i,j) = max{0, ∑ k = i j \sum_{k=i}^j k=ijAk}的最大值。

算法1.

int MaxSubseqSum1( int List[], int N )
{
	int i, j, k;
	int ThisSum, MaxSum = 0;

	for ( i=0; i<N; i++ ) {     /* i是子列左端位置 */
		for ( j=i; j<N; j++ ) { /* j是子列右端位置 */
			ThisSum = 0;  /* ThisSum是从List[i]到List[j]的子列和 */
			for ( k=i; k<=j; k++ )
				ThisSum += List[k];
			if ( ThisSum > MaxSum ) /* 如果刚得到的这个子列和更大 */
				MaxSum = ThisSum;   /* 则更新结果 */
		} /* j循环结束 */
    } /* i循环结束 */
	return MaxSum;
}

这个算法的时间复杂度为T(N)=O(N3)

算法2. 将算法一改进——>去掉了累加循环k

int MaxSubseqSum2( int List[], int N )
{
	int i, j;
	int ThisSum, MaxSum = 0;

	for( i=0; i<N; i++ ) {     /* i是子列左端位置 */
		ThisSum = 0;  /* ThisSum是从List[i]到List[j]的子列和 */
		for( j=i; j<N; j++ ) { /* j是子列右端位置 */
			/*对于相同的i,不同的j,只要在j-1次循环的基础上累加1项即可*/
			ThisSum += List[j];
			if( ThisSum > MaxSum ) /* 如果刚得到的这个子列和更大 */
				MaxSum = ThisSum;   /* 则更新结果 */
		} /* j循环结束 */
    } /* i循环结束 */
	return MaxSum;
}

这个算法只有两重循环的嵌套,所以时间复杂度是T(N)=O(N2)

算法3.分治法

分治法就是把数列冲中间一分为二,然后递归的解决两边的问题,我们会分别得到左右两边的最大子列和,还有一种可能是跨越中间边界的最大子列和。找到三个子列和以后,那么最大子列和就在三个其中。

image-20221124210841621

int Max3( int A, int B, int C )
{ /* 返回3个整数中的最大值 */
	return (A>B)?((A>C)?A:C):((B>C)?B:C);
}

int DivideAndConquer( int List[], int left, int right )
{ /* 分治法求List[left]到List[right]的最大子列和 */
	int MaxLeftSum, MaxRightSum; /* 存放左右子问题的解 */
	int MaxLeftBorderSum, MaxRightBorderSum; /*存放跨分界线的结果*/

	int LeftBorderSum, RightBorderSum;
	int center, i;
	/* 
	递归的终止条件,子列只有1个数字
	当该数为正数时,最大子列和为其本身
	当该数为负数时,最大子列和为 0
    */
	if( left == right ) {
		if( List[left] > 0 )  return List[left];
		else return 0;
	}

    /* 下面是"分"的过程 */
	center = ( left + right ) / 2; /* 找到中分点 */
	/* 递归求得两边子列的最大和 */
	MaxLeftSum = DivideAndConquer( List, left, center );
	MaxRightSum = DivideAndConquer( List, center+1, right );

    /* 下面求跨分界线的最大子列和 */
	MaxLeftBorderSum = 0; LeftBorderSum = 0;
	for( i=center; i>=left; i-- ) { /* 从中线向左扫描 */
		LeftBorderSum += List[i];
		if( LeftBorderSum > MaxLeftBorderSum )
			MaxLeftBorderSum = LeftBorderSum;
	} /* 左边扫描结束 */

	MaxRightBorderSum = 0; RightBorderSum = 0;
	for( i=center+1; i<=right; i++ ) { /* 从中线向右扫描 */
		RightBorderSum += List[i];
		if( RightBorderSum > MaxRightBorderSum )
			MaxRightBorderSum = RightBorderSum;
	} /* 右边扫描结束 */

    /* 下面返回"治"的结果 */
	return Max3( MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum );
}

int MaxSubseqSum3( int List[], int N )
{ /* 保持与前2种算法相同的函数接口 */
	return DivideAndConquer( List, 0, N-1 );
}

算法4.在线处理

在线”的意思是指每输入一个数据就进行即时处理,在任何一个地方中止输入,算法都能正确给出当前的解。

int MaxSubseqSum4( int List[], int N )
{
	int i;
	int ThisSum, MaxSum;

	ThisSum = MaxSum = 0;
	for ( i=0; i<N; i++ ) {
		ThisSum += List[i]; /* 向右累加 */
		if ( ThisSum > MaxSum )
			MaxSum = ThisSum; /* 发现更大和则更新当前结果 */
		else if ( ThisSum < 0 ) /* 如果当前子列和为负 */
			ThisSum = 0; /* 则不可能使后面的部分和增大,抛弃之 */
	} 
	return MaxSum;
}

好处是只有一个for循环,它的时间复杂度为T(N)=O(N),但是缺点是他最后得到的结果并不是特别准确

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
引用\[1\]:在C++中,数据结构是指一种组织和存储数据的方式。在这段代码中,使用了结构体来定义一个节点(node),节点包含了两个整数变量x和y。结构体中还重载了小于号运算符,用于比较节点的大小。主函数中使用了优先队列(priority_queue)来存储节点,并按照节点的x值从大到小进行排序。然后通过遍历优先队列,输出节点的x和y值。\[1\] 引用\[2\]:在C++中,vector是一种动态数组容器。它可以根据需要自动调整大小,并且支持多种构造函数。例如,可以使用默认构造函数创建一个空的vector,也可以使用拷贝构造函数将一个vector的元素拷贝给另一个vector。另外,还可以使用带有两个迭代器参数的构造函数,将一个区间内的元素拷贝给vector,或者使用带有一个整数参数和一个元素参数的构造函数,将指定数量的相同元素拷贝给vector。\[2\] 引用\[3\]:这段代码是一个关于图的遍历的例题。首先,根据输入的节点数量n和边的数量m,使用并查集来判断图是否联通。然后,统计图中奇点的数量,如果奇点的数量为0或者2,则存在欧拉回路。最后,根据判断结果输出相应的结果。\[3\] 综上所述,C++中的数据结构基本概念包括使用结构体来定义节点,使用优先队列来排序节点,使用vector来存储动态数组,以及使用并查集来判断图的连通性和欧拉回路的存在性。 #### 引用[.reference_title] - *1* *2* [C++之STL基础概念、容器、数据结构](https://blog.csdn.net/Pxx520Tangtian/article/details/126764518)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v4^insert_chatgpt"}} ] [.reference_item] - *3* [c++数据结构-图(详解附算法代码,一看就懂)](https://blog.csdn.net/m0_64036070/article/details/128737229)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v4^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tian Meng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值