数据结构——算法的时间复杂度

前言

今天我们开始学习数据结构。

那么这里有一个问题,你知道我们电脑是怎样存储数据的? 

内存磁盘是电脑存储数据的两个核心介质。

不同在于,数据结构是在内存中管理数据。它的速度快并且是带电存储。例如我们学过的通讯录(数组顺序表),以及链表、树、哈希图、图等等...

数据库是在磁盘上管理数据。它的速度慢且不带电存储。永久保存,数据库也是对数据的增删查改等功能实现。

1. 数据保存在内存
优点:存取速度快
缺点:数据不能永久保存

2. 数据保存在文件
优点:数据永久保存
缺点:(1) 速度比内存操作慢,频繁的IO操作 ;(2) 查询数据不方便

3. 数据保存在数据库
(1) 数据永久保存
(2) 使用SQL语句,查询方便、效率高
(3) 管理数据方便

磁盘上的简单数据都存储到文件中,复杂的文件都存储到数据库中。

什么时候需要在内存底下和在磁盘底下管理数据呢?

举个例子,当我们电脑打开微信,我们需要增删查改的信息时,这些信息都是存储在我们电脑的内存上的,磁盘上是找不到这些人的信息的。那为什么我们关闭电脑再次打开,这些信息还是存在呢?

它实质上是永久存储在腾讯的服务器上的磁盘上。

为什么不会存储在我们的磁盘上?

因为磁盘运行的速度太慢。不方便增删查改。
 

1.  什么是数据结构?

  数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的 数据元素的集合。  

2. 什么是算法?

  算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。 

3.算法效率

 
3.1如何去衡量一个算法的好坏?

在C语言中我们学习了递归。比如用递归实现斐波那契数列:

long long Fib(int N)
{
    if(N < 3)
        return 1;

    return Fib(N-1) + Fib(N-2);
}


斐波那契数列的递归实现方式非常简洁,但简洁一定好吗?那该如何衡量其好与坏呢?

我们引入“算法复杂度”的概念:

3.2 算法的复杂度

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度空间复杂度

时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。 所以,在实际中我们谈论较多就是时间复杂度,即程序运行的效率和速度。
 

4. 时间复杂度

4.1 时间复杂度的概念  

时间复杂度的定义:在计算机科学中, 算法的时间复杂度是一个函数 ,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。
但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法 的时间复杂度。
即:找到某条基本语句与问题规模 N 之间的数学表达式,就是算出了该算法的时间复杂度。
请计算一下 Func1 ++count 语句总共执行了多少次?
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);
}

Func1 执行的基本操作次数 :
                                          

  • N = 10         F(N) = 130
  • N = 100       F(N) = 10210
  • N = 1000     F(N) = 1002010
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要 大概执行次数,那么这 里我们使用大 O 的渐进表示法。

4.2 大O的渐进表示法
 

O符号(Big O notation):是用于描述函数渐进行为的数学符号。

 推导大O阶方法:

用常数1取代运行时间中的所有加法常数。所有常数都是O(N)
在修改后的运行次数函数中,只保留最高阶项。(取决定性的项)。
如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

使用大O的渐进表示法以后,Func1的时间复杂度为:
                                                                      O(N²)
N = 10                 F(N) = 100
N = 100               F(N) = 10000
N = 1000             F(N) = 1000000

通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。


【预期管理法】

另外有些算法的时间复杂度存在最好、平均和最坏情况
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)。


注意

双层嵌套循环不一定是O(N^2)!!!要根据思想来求时间复杂度。

4.3 常见时间复杂度计算举例  

示例1

请计算一下Func1中++count语句总共执行了多少次?

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);
}

 使用大O的渐进表示法以后,Func1的时间复杂度为

                                                                               O(N²)

示例2

计算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);
}

 

示例3

计算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;
	}
	printf("%d\n", count);
}

示例4

计算Func4的时间复杂度?

void Func4(int N)
{
	int count = 0;
	for (int k = 0; k < 100; ++k)
	{
		++count;
	}
	printf("%d\n", count);
}

 实例4基本操作执行了10次,通过推导大O阶方法,时间复杂度为 O(1)。

O(1)并不是代表1次,代表的是常数次。 

示例5

计算strchr的时间复杂度?

const char * strchr ( const char * str, int character );

strchr 是一个标准 C 库函数,它用于在字符串中查找第一次出现指定字符的位置。 

实例5基本操作执行最好1次,最坏N次,时间复杂度一般看最坏,时间复杂度为 O(N)

示例6 

计算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;
	}
}

也许你会直接写出O(N²)。两层循环就一定是O(N²)了吗?

其实不是。我们要学会:从思想上算时间复杂度。

分析代码我们可以知道,这是一个冒泡排序法,第一趟交换n-1次,第二趟交换n-2次...,一共有

n-1趟,第n-1趟交换1次,故一共有(((n-1)+1)(n-1))/2 = n(n-1)/2 。可以看成O(N²)。

left和right一起把数组遍历了一遍,所以时间复杂度是O(N)。

示例7

计算BinarySearch的时间复杂度?(二分查找的时间复杂度)

【关于二分查找】:http://t.csdnimg.cn/8Qwsf

int BinarySearch(int* a, int n, int x)
{
	assert(a);
	int begin = 0;
	int end = n - 1;
	while (begin <= end) // 因为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;
}

最好的情况是一下就找到,

最坏的情况是找到最后还剩下一个值的时候才找到或者没有找到。

要注意:时间复杂度是指调用的次数

通常情况下log的下标2可以省略,也就是可以简写为O(logN)。  只有下标为2才可以简写为这样。

有些书上博客上简写为lgN,不建议这样写,因为在数学里lg下表为10。

可以用二分查找的前提是必须是一个有序数组。二分查找对比暴力查找(直接查找)的优势还是很大的。但是前提是有序,后期在增删查改模块很麻烦。以后我们会学习AVL树 红黑树 哈希表等来解决此类问题。

 示例8

计算阶乘递归Fac的时间复杂度。

long long Fac(size_t N)
{
	if (0 == N)
		return 1;
	return Fac(N - 1) * N;
}

时间复杂度为O(N)   。

递归调用是多次调用累加。

每次递归调用都是O(1),叠加N+1次,即O(N)。

示例8变形 

计算阶乘递归Fac的时间复杂度。

long long Fac(size_t N)
{
	if (0 == N)
		return 1;
	for (size_t i = 0; i < N; ++i)
	{
		//.....
	}
	return Fac(N - 1) * N;
}

时间复杂度为O(N²)。    ((N+0)(N+1))/2

递归N次调用,每次调用的值都不确定,从N->0。递归次数累加,是一个等差数列。

示例9

计算斐波那契递归Fib的时间复杂度。

long long Fib(size_t N)
{
	if (N < 3)
		return 1;
	return Fib(N - 1) + Fib(N - 2);
}

时间复杂度为O(2^N)。

 最后用等比数列求和,算出总调用次数即可。

这里求和时用到了错位相减法,这里不再进行详细讲解。

http://t.csdnimg.cn/bCH04

参考这篇文章,我们用迭代的方法去解决斐波那契数列问题,它的时间复杂度是O(N),比递归要好一点。斐波那契数列求和用递归的方法思想简单,但是实践意义不大。因为它的时间复杂度太高。(指数型爆炸函数!)

5 单身狗

一个数组中只有一个数字出现一次,其他所有数字都是成对出现。编写一个函数找出这个数字。

【方法1】暴力求解:统计每个元素出现的次数,然后找出只出现一次的。


【方法2】异或

 异或操作符:相同为0,相异为1

  • a^a=0
  • a^0=a
  • a^b^a=a^a^b=b(异或支持交换律)
#include<stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,1,2,3,4,5 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	int ret = 0;
	for (i = 0; i < sz; i++)
	{
		ret ^= arr[i];
	}
	printf("%d ", ret);
	return 0;
}

 输出为6。

单身狗2

 一个数组中只有两个数字出现一次,其他所有数字都出现了两次。编写一个函数找出这两个只出现一次的数字。

【方法1】暴力求解


【方法2】分组异或

思路:

分组:把两个单身狗分为两组,再利用单身狗1异或的方法分别求解。

先数组全部元素异或到一起得到的结果就是两个单身狗异或的结果。
因为两个单身狗一定不相同,所以异或的结果一定不为0,根据相同为0,相异为1的原理,结果中二进制位一定有1,选择为1的位数,来进行分组,这样就可以把两个单身狗放到不同的分组里面了。
只要异或之后的结果为1的位数,就可以拿来分组
若&1 == 1,则此位为1         若&1 == 0 ,则此位为0

#include<stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,6,1,2,3,4,5 };//5 6
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	int ret = 0;
	//1.全部^到一起
	for (i = 0; i < sz; i++)
	{
		ret ^= arr[i];
	}
	//2.找到为1的n位
	int n = 0;//n是为1的位数
	for (n = 0; n < 32; n++)//4个字节32个bite位
	{
		if (((ret>>n) & 1 )== 1)
		{
			break;//n是移动几位,第几位
		}
	}
	//3.分组
	int r1 = 0;
	int r2 = 0;
	for (i = 0; i < sz; i++)
	{
		if (((arr[i] >> n)&1) == 1)
		{
			r1 ^= arr[i];
		}
		if (((arr[i] >> n) & 1) == 0)
		{
			r2 ^= arr[i];
		}
	}
	printf("r1=%d r2=%d\n", r1, r2);
	//返回下标
	int j = 0;
	for (j = 0; j < sz; j++)
	{
		if (arr[j] == r1)
			printf("r1下标:%d\n", j);
		if (arr[j] == r2)
			printf("r2下标:%d\n", j);
	}
	return 0;
}
//封装成函数-----想把两个单身狗带回-----用指针
//返回下标-----------遍历一遍

6 消失的数字

数组nums包含从0n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?动手写一写。 

【方法1】暴力求解:先冒泡排序,再遍历一遍,当前值+1  不等于下一个值 这就是消失的数字

冒泡排序时间复杂:O(N^2)  遍历时间复杂度:O(N) 。 时间复杂度:O(N^2)


【方法2】异或

第一个循环时间复杂度:O(N)  第二个循环时间复杂度: O(N+1)   时间复杂度:O(N)

int missingNumber(int* nums, int numsSize){
    int ret=0;
    int i=0;
    for(i=0;i<numsSize;i++)//先把数组全部数组异或 0~N缺少一个数字所以是N+1-1个数字即N个数字
    {
        ret^=nums[i];
    }
    for(i=0;i<=numsSize;i++)//得到的结果再次异或0~N数字 0~N有N+1个数字
    {
        ret^=i;
    }
    return ret;
}

【方法3】等差数列公式计算:0~N等差数列公式计算和,再一次减去数组中的元素,剩下的就是消失的数字。

  • 等差数列时间复杂度:O(1)  循环时间复杂度:O(N) 时间复杂度:O(N)
int missingNumber(int* nums, int numsSize)
{
    int N = numsSize;
    int ret=((0+N)*(N+1))/2;  //0~N有N+1个数!
    int i=0;
    for(i=0;i<numsSize;i++)
    {
        ret-=nums[i];
    }
    return ret;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值