“数据结构”是计算机软件相关专业的基础课程,几乎可以说,要想从事编程工作,无论你是否是科班出身,都绕不开这部分知识。
下面,将对一些概念和术语赋以确定的含义。
数据(data):是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。对计算机科学而言,数据的含义极为广泛,如图像、声音等都可以通过编码而归至于数据的范畴。
我们这里说的数据,其实就是符号,而且这些符号必须具备两个前提:
可以输入到计算机中、能被计算机程序处理。
数据元素(data element):是组成数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理。
数据项(data item):一个数据元素可以由若干个数据项组成。数据项是数据不可分割的最小单位。
数据对象(data object):是性质相同的数据元素的集合,是数据的一个子集。
数据结构(data structure):是相互之间存在一种或多种特定关系的数据元素的集合。
按照视点的不同,我们把数据结构分为逻辑结构和物理结构。
物理结构是指数据的逻辑结构在计算机中的存储形式。
顺序存储结构
顺序存储结构是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。在之前学习的C语言中,数组就是这样的顺序存储结构。
链式存储结构
**链式存储结构是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。**数据元素的存储关系并不能反映其逻辑关系,因此需要用一个指针存放数据元素的地址,这样通过地址就可以找到相关数据元素的位置。
ADT 抽象数据类型名
{
数据对象:<数据对象的定义>
数据关系:<数据关系的定义>
基本操作:<基本操作的定义>
}
其中,数据对象和数据关系的定义用伪码描述,基本操作的定义格式为:
基本操作名(参数表)
初始条件:<初始条件描述>
操作结果:<操作结果描述>
算法执行时间需通过依据该算法编制的程序在计算机上运行时所消耗的时间来度量。而度量一个程序的执行时间通常有两种方法。
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作T(n)= O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f( n )是问题规模n的某个函数。
这样用大写O( )来体现算法时间复杂度记法,我们称之为大O记法。
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
另外有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
// 计算Func4的时间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
看上面这个例子,for循环共循环了100次,通过推导大O阶方法,时间复杂度为 O(1)。所以,循环次数是确定的常数次,时间复杂度都是O(1)。
// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n;
while (begin < end)
{
int mid = begin + ((end - begin) >> 1);
if (a[mid] < x)
begin = mid + 1;
else if (a[mid] > x)
end = mid;
else
return mid;
}
return -1;
}
这就是C语言中的二分查找,前提是这个数组是有序的时候,才可以用二分查找,如果用一般的遍历数组的话,时间复杂度应该是O(n),而这里用的二分查找就使得时间复杂度变成了O(logn),这使得程序所消耗的时间大大减少了。
// 计算Func2的时间复杂度?
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
看上面的代码,基本操作执行了2N+10次,通过推导大O阶方法知道,时间复杂度为 O(N)。
// 计算strchr的时间复杂度?
const char * strchr ( const char * str, char character )
{
while(*str != '\0')
{
if(*str == character)
return str;
++str;
}
return NULL;
}
在上面的代码中,不知道字符串的具体长度,基本操作执行最好1次,最坏N次,时间复杂度一般看最坏,所以时间复杂度为 O(N)。
// 计算Func3的时间复杂度?
void Func3(int N, int M)
{
int count = 0;
for (int k = 0; k < M; ++ k)
{
++count;
}
for (int k = 0; k < N ; ++ k)
{
++count;
}
上面代码中有两个未知数,基本操作执行了M+N次,有两个未知数M和N,时间复杂度为 O(N+M)。
// 计算BubbleSort的时间复杂度?
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i-1] > a[i])
{
Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
上面的代码是C语言中的冒泡排序算法,它的原理就是每次排出一个数出来,基本操作执行最好N次,最坏执行了(N*(N+1)/2次,通过推导大O阶方法+时间复杂度一般看最坏,时间复杂度为 O(N^2)。
我们这—章主要谈了算法的一些基本概念。谈到了数据结构与算法的关系是相互依赖不可分割的。
算法的定义:算法是解决特定问题求解步骤的描述,在计算机中为指令的有限序列,并且每条指令表示一个或多个操作。
算法的特性:有穷性、确定性、可行性、输入、输出。
算法的设计的要求:正确性、可读性、健壮性、高效率和低存储量需求。
算法特性与算法设计容易混,需要对比记忆。
算法的度量方法:事后统计方法(不科学、不准确)、事前分析估算方法。
然后给出了算法时间复杂度的定义和推导大O阶的步骤。
推导大O阶:
(1)用常数1取代运行时间中的所有加法常数。
(2)在修改后的运行次数函数中,只保留最高阶项。
(3)如果最高阶项存在且其系数不是1,则去除与这个项相乘的系数。
得到的结果就是大O阶。
通过这些步骤,我们可以在得到算法的运行次数表达式后,很快得到它的时间复杂度,即大O阶。同时我也提醒了大家,其实推导大O阶很容易但如何得到运行次数的表达式却是需要数学功底的。
时间复杂度算的不是时间是执行次数,空间复杂度算的不是空间是变量的个数。