于准备考研第一轮开始看这本数据结构,记录一些学习总结还有从陈越姥姥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;
}
上面后三种算法有些比较合理的使用了递归的方法,都使问题变得简单并减少了运算的次数。