一、数据结构
结构,简单的理解就是关系。结构是指各个组成部分相互搭配和排列的方式。在现实世界中,不同数据元素之间不是独立的,而是存在特定的关系,将这些关系称为结构。
数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。
在计算机中,数据元素并不是孤立、杂乱无序的,而是具有内在联系的数据集合。数据元素之间存在的一种或多种特定关系,也就是数据的组织形式。
数据结构分为逻辑结构和物理结构。物理结构是在数据内存中如何存储,而逻辑结构是靠我们想象的。
(一)逻辑结构
逻辑结构:是指数据对象中数据元素之间的相互关系。这也是最需要关注的问题。逻辑结构分为以下四种:
1.集合结构
集合结构:集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。各个数据元素是“平等”的,它们的共同属性是“同属于一个集合”。数据结构中的集合关系就类似于数学中的集合。
2.线性结构
线性结构:线性结构中的数据元素之间是一对一的关系。
3.树形结构
树形结构:树形结构中的数据元素之间存在一种一对多的层次关系。
4.图形结构
图形结构:图形结构的数据元素是多对多的关系。
(二) 物理结构
数据的物理结构(也称为做存储结构):是指数据的逻辑结构在计算机中的存储形式。
数据是数据元素的集合,根据物理结构的定义,实际上就是如何把数据元素存储到计算机的存储器中。存储器主要是针对内存而言的,像硬盘、软盘、光盘等外部存储器的数据组织通常用文件结构来描述。
数据元素的存储结构形式有两种:顺序存储和链式存储。
1、顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。数组就是这样的顺序存储结构,数组在物理结构和逻辑结构上都是连续的。
2、链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。数据元素的存储关系并不能反映其逻辑关系,因此需要用一个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置。
链式存储比较灵活,数据存在哪里不重要,只要有指针能够找到它就行。
逻辑结构是面向问题的,而物理结构就是面向计算机的,其基本的目标就是将数据及其逻辑关系存储到计算机的内存中。
二、算法
算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
为了解决某个或某类问题,需要把指令表示成一定的操作序列,操作序列包括一组操作,每一个操作都完成特定的功能,这就是算法了。
算法的特性:有穷性、确定性、可行性、输入、输出。
算法的设计的要求:正确性、可读性、健壮性、高效率和低存储量需求。
效率一般指时间效率,执行时间短的算法效率高,执行时间长的效率低。
存储量需求指的是算法在执行过程中需要的最大存储空间,主要指算法程序运行时所占用的内存或外部硬盘存储空间。设计算法应该尽量满足时间效率高和存储量低的需求。
要衡量一个算法的效率,需要时间复杂度和空间复杂度来度量 。
三、时间复杂度与空间复杂度
(一)时间复杂度
在进行算法分析时,语句总的执行次数 T(n) 是关于问题规模n的函数,进而分析 T(n) 随n 的变化情况并确定 T(n) 的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)= O(f(n))。其中 f(n) 是问题规模 n 的某个函数。
这样用大写 O() 来体现算法时间复杂度的记法,称之为大О记法。一般情况下,随着 n的增大,T(n) 增长最慢的算法为最优算法。
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
++count;
}
}
for (int k = 0; k < 2 * N; ++k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
在上面的代码中,调用Fun1总共会执行 f(n)=n^2+2*n+10 次。
而在实际中计算时间复杂度,不一定要计算精确的执行次数,只需要大概执行次数,也就是简化函数,将一些对公式最后结果影响相对较小的式子和常数省略。
如何简化函数?
由于执行次数 T(n)= O(f(n)),要简化函数,使用大O的渐进表示法:
(1)用常数1取代运行时间中的所有加法常数。
(2)在修改后的运行次数函数中,只保留最高阶项。
(3)如果最高阶项存在且不是1,则去除与这个项相乘的常数。得到的结果就是大О阶。
对于公式 f(n)=n^2+2*n+10,可将10和2*n省略,只保留n^2,如果n^2还存在常数项系数,比如3,也可以省略,此时使用大O的渐进表示法以后,Func1的时间复杂度为:
O(n^2)
1、例1
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);
}
f(n)=2*n+10,使用大O的渐进表示法后,时间复杂度为:O(n)
2、例2
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;
}
printf("%d\n", count);
}
f(n)=m+n,使用大O的渐进表示法后,时间复杂度为:O(m+n);
如果有明确提示m和n的大小关系:
(1)m远大于n,则可以将n省略,时间复杂度为:O(m)。
(2)m远小于n,则可以将m省略,时间复杂度为:O(n)。
(3)m和n差不多大,时间复杂度可表示为:O(m)或者O(n)。
3、例3
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
f(n)=100;
大O的渐进表示法 (1)用常数1取代运行时间中的所有加法常数。
使用大O的渐进表示法后,时间复杂度为:O(1);
也就是说时间复杂度O(1)并不意味着只运行一次,而是运行常数次。
4、例4
const char* strchr(const char* str, int character);
strchar是在字符串中查找一个字符。
此时可分为多种情况:
(1)最好的情况:只找一次,将找到目标字符,此时时间复杂度为O(1);
(2)最坏的情况:遍历整个字符串才找到,此时时间复杂度为:O(n);
(3)平均情况:在最好和最坏之间,时间复杂度:O(n/2);
最坏情况运行时间是一种保证,那就是运行时间将不会再坏了。在应用中,这是一种最重要的需求,通常,除非特别指定,平常提到的运行时间都是最坏情况的运行时间。
5、例5:冒泡排序
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;
}
}
最坏情况,遍历两个循环,此时 f(n)=n-1 + n-2 + ... + 3 + 2 + 1 = (n*(n+1)/2;
使用大O的渐进表示法后, 时间复杂度是O(n^2)
6、例6:二分查找
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n - 1;
while (begin <= end)
{
int mid = begin + ((end - begin) >> 1);
if (a[mid] < x)
begin = mid + 1;
else if (a[mid] > x)
end = mid - 1;
else
return mid;
}
return -1;
}
二分查找每次查找,能够筛选一半的数据, 时间复杂度:O(log n)。
7、例7:递归
long long Fac(size_t N)
{
if (0 == N)
return 1;
return Fac(N - 1) * N;
}
递归算法的时间复杂度: 递归次数*每次函数调用的次数。
时间复杂度:O(n)
8、例8:斐波那契递归
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
时间复杂度:O(2^n)
(二)空间复杂度
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=o(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数,也使用大O渐进表示法。
空间复杂度不是程序占用了多少bytes的空间,空间复杂度算的是变量的个数。 也就是说空间复杂度不计算空间,计算大概定义的变量个数。
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;
}
}
对于冒泡排序,函数只创建了常数个空间,空间复杂度为:O(1)。
long long* Fibonacci(size_t n)
{
if (n == 0)
return NULL;
long long* fibArray = (long long*)malloc((n + 1) * sizeof(long long));
fibArray[0] = 0;
fibArray[1] = 1;
for (int i = 2; i <= n; ++i)
{
fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
}
return fibArray;
}
函数利用malloc开辟了n+1个空间,空间复杂度为:O(n)。