前言
今天我们开始学习数据结构。
那么这里有一个问题,你知道我们电脑是怎样存储数据的?
内存和磁盘是电脑存储数据的两个核心介质。
不同在于,数据结构是在内存中管理数据。它的速度快并且是带电存储。例如我们学过的通讯录(数组顺序表),以及链表、树、哈希图、图等等...
数据库是在磁盘上管理数据。它的速度慢且不带电存储。永久保存,数据库也是对数据的增删查改等功能实现。
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) = 100N = 100 F(N) = 10000N = 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 库函数,它用于在字符串中查找第一次出现指定字符的位置。
示例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)。
最后用等比数列求和,算出总调用次数即可。
这里求和时用到了错位相减法,这里不再进行详细讲解。
参考这篇文章,我们用迭代的方法去解决斐波那契数列问题,它的时间复杂度是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
包含从0
到n
的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在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;
}