《数据结构与算法分析——C语言描述》 第一章、第二章 基础概念 学习总结

于准备考研第一轮开始看这本数据结构,记录一些学习总结还有从陈越姥姥MOOC中总结的一些内容,方便自己查阅观看,最后希望跨考计算机能够成功。

首先需要明白数据结构与算法密不可分,然后注意三个点:
(1)解决问题方法的效率,与数据的组织方式有关;
(2)解决问题方法的效率,也跟空间的利用效率有关;
(3)解决问题方法的效率,跟算法的巧妙程度有关。

看完王道第一章——绪论之后补充一些基本概念(简要记录一下):
(1)数据:所有能输入计算机并被计算机识别和处理的符号的集合。
(2)数据元素:数据元素是数据的基本单位,一个数据元素由若干数据项组成,数据项是数据元素不可分割的最小单位。(例如把学生记录当做一个数据元素,那么学生的学号、年龄、性别等就是组成数据元素的数据项)
(3)数据对象:具有相同性质的数据元素的集合,是数据的一个子集。
(4)数据类型:一个值的集合和在此集合上的一组操作的总称。
(5)抽象数据类型:指一个数学模型和定义在该模型上的一组操作。(描述了数据的逻辑结构和抽象运算),通常用(数据对象、数据关系、基本操作集)这样的三元组来表示抽象数据类型。
(6)数据结构:指相互之间存在一种或多种特定关系的数据元素的集合。数据元素相互之间的关系称为结构,数据元素包含三部分内容:逻辑结构、存储结构、数据的运算(缺一不可)。一个算法的设计取决于所选定的逻辑结构,而算法的实现依赖于所采用的存储结构。

数据结构三要素:
(1)逻辑结构:指数据元素之间的逻辑关系,它与数据的存储无关,是独立于计算机的。逻辑结构分为线性结构(线性表、栈、队列)和非线性结构(树、图、集合)。
(2)存储结构:指数据结构在计算机中的表示(映像),也称物理结构。它包含数据元素的表示和关系的表示,数据的存储结构是用计算机语言实现的逻辑结构,它依赖于计算机语言,相当于存储结构是逻辑结构在计算机中的映像,所以存储结构依赖于逻辑结构。存储结构主要由:顺序存储(数组)、链式存储(链表)、索引存储、散列存储。

数据对象必定与一系列加在其上的操作相关联。
完成这些操作所用的方法就是算法。

关于如何计算程序运行时间(以一个简单的多项式求和(C)为例):

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

double f( int n, double a[], double x );
#define MAX 10000000
#define N 10
clock_t start, stop;

int main ( void )
{
	int i;
	double duration;
	double a[N];
	
	for( i = 0; i < N; i++ )
		a[i] = i;
		
	start = clock();
	for( i = 0; i < MAX; i++ )
		f( N - 1, a, 1.1 );
	stop = clock();
	
	duration = (double)( stop - start ) / CLK_TCK;
	printf( "%f", duration );
	
	return 0;
}

double f( 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;
}

(其中计算多项式的和的方法值得思考,它比直接的从数组第一个元素顺序加到最后一个元素的算法要更好,更加实用,因为它的时间复杂度是O(N),而顺序相加时间复杂度是O(N^2)。

接下来是概念抽象数据类型(Abstract Data Type:ADT):
数据类型一般主要包括的要素是:
(1)数据对象集;
(2)数据集合相关联的操作集。
抽象指的是描述数据类型的方法不依赖于具体实现,具体指的是:
(1)与存放数据的机器无关;
(2)与数据存取的物理结构无关;
(3)与实现操作的算法和编程语言均无关。
综上,就是说抽象数据类型只描述数据对象集和相关操作集是什么,并不涉及如何做到的问题。

算法一般指:
(1)一个有限的指令集;
(2)接受一些输入(也可以没有输入);
(3)会产生输出;
(4)一定在有限的步骤之后终止;
(5)每一条指令必须:
(i)有充分明确的目标,不可以有歧义;
(ii)在计算机能处理的范围内;
(iii)描述应不依赖于任何一种计算机语言以及具体的实现手段。

所以在编写一些通用的函数但对应的变量类型不相同时,可以使用#define宏把函数返回的变量类型定义成ElementType,在使用具体的变量类型时(例如int, float, double或struct)使用宏把ElementType定义成对应的类型即可。

空间复杂度S(n):根据算法写成的程序在执行时占用存储单元的长度。
时间复杂度T(n):根据算法写成的程序在执行时耗费时间的程度。
其中S指space,T指time。

关于时间复杂度的一些知识点:
我们一般关注的两种复杂度分别是:
(1)Tavg(n)(平均复杂度);
(2)Tworst(n)(最坏情况复杂度)。
在这两种复杂度里面,一般我们要找的是最坏情况复杂度,一是方面因为平均复杂度本身就比较难求解,另一方面是因为最坏情况复杂度为我们的输入提供了一个界限。
一般情况下我们不需要仔细地去分析算法的时间复杂度,我们只需要知道算法的增长趋势就足够了。

关于复杂度渐进表示的一些内容比较简单,暂且不提。

接下来是使用递归的四条基本法则:
(1)基准情形;(简单来说就是递归的出口)
(2)不断推进;(递归的程序要有不断向递归出口靠近的趋势)
(3)设计法则;
(4)合成效益法则。(不要让递归出现重复的部分:典型反例就是输出斐波那契数列)

接下来是最大子列和问题的四种解法(C语言,算法复杂度依次是:O(N^3)、O(NN)、O(NlogN)、O(N)):
这里使用PAT上的输入输出示例。

算法1:
暴力拆解,三重循环逐项相加。

int f1( int a[], int n )
{
	int i, j, k;
	int thissum, maxsum = 0;
	for( i = 0; i < n; i++ ){
		for( j = i; j < n; j++ ){
			thissum = 0;
			for( k = i; k <= j; k++ ){
				thissum += a[k];
			}
			if( thissum > maxsum )
				maxsum = thissum;
		}
	}
	return maxsum;
}

算法2:
双重循环,省略掉算法1中每一次都从头加起的多余步骤(也就是算法1中的k循环)。

int f2( int a[], int n )
{
	int i, j;
	int thissum, maxsum = 0;
	for( i = 0; i < n; i++ ){
		thissum = 0;
		for( j = i; j < n; j++ ){
			thissum += a[j];
			if( thissum > maxsum )
				maxsum = thissum;
		}
	}
	return maxsum;
}

算法3:
分治法,分而治之,递归解决问题。这个算法每一次将数组分为左右两部分,然后分别在左右两部分中求最大子列和,然后再求跨越左右两部分的最大子列和,从中选出最大的一项输出。

int f3( int a[], int left, int right )
{
	if( left == right )
		if( a[left] > 0 )
			return a[left];
		else
			return 0;
	int middle = ( left + right ) / 2, i;
	int max_left_sum = f3( a, left, middle );
	int max_right_sum = f3( a, middle + 1, right );
	
	int max_left_border_sum = 0, left_border_sum = 0;
	for( i = middle; i >= left; i-- ){
		left_border_sum += a[i];
		if( left_border_sum > max_left_border_sum )
			max_left_border_sum = left_border_sum;
	} 
	
	int max_right_border_sum = 0, right_border_sum = 0;
	for( i = middle + 1; i <= right; i++ ){
		right_border_sum += a[i];
		if( right_border_sum > max_right_border_sum )
			max_right_border_sum = right_border_sum;
	} 
	
	int max = max_left_sum;
	if( max_right_sum > max )
		max = max_right_sum;
	if( max_left_border_sum + max_right_border_sum > max )
		max = max_left_border_sum + max_right_border_sum;
	
	return max;
}

算法4:
在线处理算法,只需要遍历一次数组元素,“在线”的意思是指每输入一个数据就进行即时处理,在任何地方终止输入,算法都能给出当前的解。

int f4( int a[], int n )
{
	int i;
	int thissum = 0, maxsum = 0; 
	for( i = 0; i < n; i++ ){
		thissum += a[i];
		if( thissum > maxsum )
			maxsum = thissum;
		else if( thissum < 0 )
			thissum = 0;
	}
	return maxsum;
}

关于四种算法运行时间的比较,处于方便直接放PAT的测试结果:
算法1:无法给出结果;
算法2:

算法3:

算法4:

对数出现的一般法则:
如果一个算法用常数时间(O( 1 ))将问题的大小削减为其一部分(通常是1/2),那么该算法的时间复杂度就是O(logN), 另一方面,如果是用常数时间只是把问题减少一个常数(比如O(1)),那么这个算法就是O(N)的。(可以参照下面几种简单的算法来理解这句话)

最后书中补充了几种简单的解决问题的算法:
(1)对分查找:
对于一组已经排好序的数据(这里递增排列),每一次都先比较中间的元素,如果大于要查找的元素,则去数组左边查找;如果小于,则去数组右边查找;如果相等,就返回中间的下标;如果没有找到,就返回0。

int binary_search( ElementType a[], ElementType x, int n )
{
	int low = 0, mid, high = n - 1;
	while( low <= high ){
		mid = ( low + high ) / 2;
		if( a[mid] == x )
			return mid;
		else if( a[mid] > x )
			high = mid - 1;
		else
			low = mid + 1;
	}
	return 0;
}

(2)欧几里得算法:
辗转相除法求最大公约数(这是我比较喜欢的写法,使用递归的方式求最大公约数):
在这里插入图片描述
(3)取幂运算:

long pow_( int x, int k )
{
	if( k == 0 )
		return 1;
	if( k == 1 )
		return x;
	if( k % 2 == 0 )
		return pow_( x * x, k / 2 );
	else
		return pow_( x * x, k / 2 ) * x;
}

上面后三种算法有些比较合理的使用了递归的方法,都使问题变得简单并减少了运算的次数。

猜你喜欢

转载自blog.csdn.net/qq_40344308/article/details/88254568