数据结构前言

124 阅读4分钟

数据结构与算法

什么是数据结构?

简单来说,在内存中存储管理数据的结构,即数据结构。

数据结构与数据库的区别?

本质上都是存储管理数据.

但数据结构 —— 在内存中存储管理数据.

数据库 —— 在磁盘中存储管理数据.

什么是算法?

按要求对数据进行某种处理的过程.如查找、排序,不同算法实现过程不同.

数据结构和算法是不分家的.

复杂度

如何衡量算法的好坏?

某个算法在运行时,需要消耗时间和占用空间;

因此用时间复杂度(描述算法运行时间) 和 空间复杂度(描述算法额外开辟的空间大小)来衡量.

时间复杂度

功能:衡量一个算法的运行快慢.

定义:时间复杂度实际是一个函数,描述运行时间,同时也是算法中基本操作的执行次数.

问题:如何描述运行时间?

误区:用机器上的执行时间描述?

同一个算法,在不同的机器上运行耗费的时间一般不相同.

例:

cpu:i7,16G内存的机器1

cpu:i3,2G内存的机器2

显然机器1运行速度更快

结论:同一个算法的运行时间和硬件配置有关,无法算出准确时间.

正确方式

一个算法花费的时间与语句的执行次数成正比例,

即语句的执行次数越多,算法花费的时间自然越多.例:

int func(int N)
{
    int row = 0;
    int col = 0;
    int ret = 0;
    for (row = 0; row < N; row++)
    {
	for (col = 0; col < N; col++)
        {
            ret++; // 该语句将执行N*N次
	}
    }
    for (row = 0; row < 3 * N; row++)
    {
	ret++;    // 该语句将执行3*N次
    }
    return ret;
}

通过计算可以算出较准确的时间复杂度函数式:F(N) = N*N+3×N+4;

但是准确的时间复杂度函数式,不方便在算法之间进行比较,于是引出了大O的渐进表示法.

大O的渐进表示法

例:F(N) = N*N+3×N+4.

随着N的增大,后两项对结果影响几乎可以忽略不计。

所以用大O渐进法表示的时间复杂度是O(N^2).

如何表示大O的渐进表示法?

用 1 取代【准确的函数式中】所有的常数;

在修改后的运行次数函数中,只保留最高次项.

例题一:

void func2(int N, int M)
{
    //较准确的时间复杂度函数式为2*N+M+100
    int i = 0;//用于遍历循环
    for (i = 0; i < 2*N; i++)
    {
	printf("xxx\n"); //2*N次
    }
    for (i = 0; i < M; i++)
    {
	printf("kkk\n");//M次
    }
    for (i = 0; i < 100; i++)
    {
	printf("lll\n");//100次
    }
}

使用大O的渐进表示法为O(N+M),有2个未知数。

准确说:

N远大于M时,O(N);
M远大于N,O(M);
M近似N,O(M)和O(N)都行

注意1:O(1)并不是表示一次,而是表示常数次。例:

//这里的时间复杂度是O(1)
void func(void)
{
    int i = 0;
    for (i = 0; i < 100; i++)
    {
	printf("kkk\n");//100次
    }
}

注意2:算时间复杂度不能去数循环层次,不一定准确,一定要看它的算法思想!!!例:

void bubbleSort(int* arr, int num)
{
    int i = 0;//冒泡排序的趟数,每一趟都能成功排序好一个数,所以只需排num-1次
    int k = 0;//一趟冒泡排序需要比较的次数
    int flag = 1;//假设一趟下来没有交换任何数
    for (i = 0; i < num - 1; i++)
    {
	for (k = 0; k < num - 1 - i; k++)//一趟冒泡排序的比较次数与i有关
        {
            if (arr[k] > arr[k + 1])//交换
            {
		int tem = arr[k];
		arr[k] = arr[k + 1];
                arr[k + 1] = tem;
		flag = 0;
            }
	}
	if (flag == 1)//一趟下来没有交换任何数,说明数组已经有序
            return;
    }
}

首先外层循环执行num - 1次,内层循环执行的次数与i有关.

内层循环执行的总次数的函数式,就是该算法的时间复杂度.

i内层循环执行次数
0num-1
1num-2
2num-3
3num-4
........
num-21

那总次数其实就是 1 + 2 + ... + num-1,根据等差公式,总次数是:

image.png

所以冒泡排序的时间复杂度是O(N^2).

注意3:有些算法的时间复杂度存在最好、平均和最坏三种情况。

以冒泡排序为例,

最好情况:第一趟冒泡排序下来没有交换任何数,说明已经有序;

最坏情况:要排序的数组元素是倒序的,要进行num-1趟才能排成有序.

在实际中,关注的是最坏的运行情况,所以时间复杂度是O(N^2).

注意4:时间复杂度是计算算法执行次数。

一个执行次数,不一定是一条语句,可能是多条语句,即常数条语句。例:

    for (i = 0; i < N; i++)
    {
	printf("kkk\n");
	printf("kkk\n");//该循环中的语句是常数条,时间复杂度的执行次数仍为N
	printf("kkk\n");
    }

实例演示1:计算阶乘的时间复杂度

//时间复杂度是O(N),因为每次递归内部的执行次数都是常数次,
//要执行N次递归
int fac(int N)
{
    if (N == 1)
	return 1;
    return N * fac(N - 1);
}

实例演示2:计算斐波那契递归的时间复杂度

//时间复杂度是O(2^n)
long long fib(int n)
{
    if (n < 3)
	return 1;
    return fib(n - 1) + fib(n - 2);
}

image.png 所以调用次数是:2^0+2^1+2^2+....+2^(n-2)=2^(n-1) - 1;

时间复杂度是O(2^n).

空间复杂度

空间复杂度也是一个数学函数式,表示一个算法临时占用存储空间的大小.

注意1:空间复杂度不是程序占用了多少byte的空间,而是算变量的个数,

也使用大O的渐进表示法。 例:

int max(int a, int b)
{
    int re = a > b ? a : b;//这里额外创建的变量只有常数个,所以空间复杂度是O(1)
    return re;
}

注意2:空间复杂度是算因为算法过程额外创建的变量数。例:

void print(int* a, int num)
{
    //空间复杂度是O(1),因为外面提供的数组大小不算
    int i = 0;
    for (i = 0; i < num; i++)
    {
	printf("%d ", a[i]);//打印数组中的所有数
    }
}

注意3:时间是累计的,但空间不会累积,是可以重复利用的。例:

void test(void)
{
    int i = 0;
    for (i = 0; i < 4; i++)
    {
	int k = 0;//在循环中定义一个变量
	printf("%p\n", &k);
    }
}

运行结果:

image.png

在循环中定义的变量,每次创建的地址都是相同的,利用的都是同一块空间,

不会在其它地方再开辟一块空间,因此空间复杂度仍为O(1).

实例演示1:计算阶乘递归的空间复杂度:

int fac(int N) 
{ 
    if (N == 1) 
        return 1; 
    return N * fac(N - 1); 
}

每次递归都要建立栈帧,这些栈帧都是因为该算法额外开的空间。

因为要递归N次,每次递归内部额外创建的变量为常数,所以空间复杂度是O(N).

实例演示2:计算斐波那契递归的空间复杂度:

long long fib(int n)
{
    if (n < 3)
	return 1;
    return fib(n - 1) + fib(n - 2);
}

image.png 它需要进行很多趟递归。但是,时间是累积的,而空间是不累计的,可以重复利用.

对于该算法,fib(N)不会同时调用fib(N-1)和fib(N-2),它是先调用左边进行递归。

同理,fib(N-1)也会先调用fib(N-2)递归……

到后面,fib(3)会先调用fib(2)创建栈帧,fib(2)返回1栈帧销毁,

然后fib(3)再调用fib(1)创建栈帧,但这块栈帧和fib(2)的栈帧共用一块空间

因此它的空间复杂度是O(N).