数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的 数据元素的集合。(该如何最佳的方式存储对应的数据,在内存当中对数据进行增删查改)
算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为 输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。(对数据进行处理运算,达到自己想要的结果)
如何衡量一个算法的好坏呢?比如对于以下斐波那契数列:
如何衡量一个算法的好坏呢?比如对于以下斐波那契数列:
long long Fib(int N) { if(N < 3) return 1; return Fib(N-1) + Fib(N-2); }
斐波那契数列的递归实现方式非常简洁,但简洁一定好吗?那该如何衡量其好与坏呢?
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般 是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。 时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算 机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计 算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一 个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知 道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个 分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法 的时间复杂度。
即:找到某条基本语句与问题规模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 执行的基本操作次数 :
F(N)= N ^ 2 + 2*N +10
当式子很长并且N很大的时候,F(N)就难以算出来,因此:
我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。(抓大头取最高阶数的项,取具有决定性结果的那一项)
所以此题的时间复杂度为:
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);
}
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;
}
printf("%d\n", count);
}
无法区分M和N的大小关系,所以都保留
O( M+N )
// 计算Func4的时间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
最大项为常数,就为O(1),1的意思为常数
O(1)
// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );
strchr意思是在一个字符串中找出一个字符,有点类似于strstr(字符串中查找字符串)
通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。 另外有些算法的时间复杂度存在最好、平均和最坏情况:
例如:
在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
时间复杂度计算时,是一个稳健保守的预期
O( N )
// 计算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;
}
}
因为要进行两两比较以第一个数向后比较,比较的数字随着比较的进行会越来越少(n-3,n-2,n-1),所以是一个等差数列,而等差数列的公式就是(N-1)*N/2 所以化简来看就是:
O(N^2)
// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n-1;
// [begin, end]:begin和end是左闭右闭区间,因此有=号
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;
}
因为时间复杂度是要计算最坏的情况,而二分查找2最坏的情况就是
查找的区间数据的个数如下:
N------------------第一次查找
N/2---------------第二次查找
N/2/2-------------第三次查找
N/2/2/2-----------第四次查找
....
N/2/2...../2/2 = 1
(每次查找减半,直到只剩一个数)
最坏情况,查找区间缩放只剩一个值时,就是最坏
最坏情况下查找多少次? 除了多少次2,就查找了多少次
假设查找x次,2^x = N,x = log(2为底数)N
我们目前看来可能二分查找不就是对半分嘛,也不会很精妙啊。下面我们可以看一下差距
假设我们写了两个程序,一个是暴力查找搜索数,一个是二分查找搜索数:
方法 搜索基数(总数) 最差情况搜索次数
暴力搜索:O(N) 1000/100W 1000/2000
二分查找:O(log(2)N) 1000/100W 10/20
我们可以看出我们如果用暴力搜索一个数的时候最次的情况就需要1000次才能找到,而100W次的时候那就需要100W次的搜索,那将是巨大的开销。
而我们用了二分查找,1000次就需要搜索10次:2^10=1000
而100W次呢?只需要2^20-->1024*1024=100W
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}
总共会调用N+1次,递归算法是多次调用的累加
所以答案为:O(N)
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
我们把具体fib的函数给展开
所以是O(2^N)
我们来做一道力扣题
面试题 17.04. 消失的数字 - 力扣(LeetCode)
因为是顺序的,所以我们可以想到类似单身狗的问题,如果想知道单身狗怎么求解可以查看我上条博客如何用C语言找到单身狗-CSDN博客
看完这道题后就会有思路啦:因为0^任何数都为任何事,而相同的数异或为0。
//原始人办法
// int missingNumber(int* nums, int numsSize) {
// int N = numsSize;
// int ret = N * (N + 1) / 2;
// for (int i = 0; i < N; i++)
// {
// ret -= nums[i];
// }
// return ret;
// }
//异或办法
int missingNumber(int* nums, int numsSize)
{
int n = numsSize;
int x = 0;
for (int i = 0; i <= n; i++)
{
x^= i; //0异或了n以内的数再异或一次缺失的数,得到的就是缺失的数
}
for (int i = 0; i < n; i++)
{
x ^= nums[i];
}
return x;
}