浅析数据结构与算法
一、什么是数据结构
数据结构是用于组织和存储数据的一种方式。它定义了一种数据的组织方式,以及在该组织结构上进行的操作。
数据结构可以看作是一种特定的数据类型,它不仅包含数据的存储方式,还包括对数据的访问、操作和组合的规则。数据结构提供了一种高效地组织和管理数据的方法,以便于对数据进行检索、插入、删除和修改等操作。
举一个简单的例子:
假设您有一本电话号码簿,里面记录了您的亲戚朋友的姓名和电话号码。为了方便查找,您可以按照姓名的首字母进行排序,然后将它们放入一个表格中。这个表格就是一个简单的数据结构,我们称之为“有序表”。
在这个有序表中,每个记录都有一个关键字,即姓名的首字母。如果您想查找某个人的电话号码,可以通过二分查找等算法,在表格中快速定位到该记录,从而获得电话号码。
这个例子中的有序表,就是一种常见的数据结构。它可以用数组、链表等方式实现,用于存储具有顺序关系的数据,并支持快速查找、插入和删除等操作。
数组就是我们学过的最简单的一个数据结构。
我们在数据结构的基础上配合算法可以实现快速高效地查找管理数据
二、什么是算法
算法是一组解决问题的明确步骤和规则,用于执行特定任务或计算的有限序列。简而言之,算法是解决问题的一种方法或过程。
举个例子:
比如,我们要做一道简单的炒青菜。下面是一个简单的算法来描述这个过程:
准备食材:洗净青菜,切成适当的大小。
加热锅:将锅放在火上,加热至适当的温度。
加油:将适量的食用油倒入锅中,等待油热。
炒菜:将切好的青菜倒入锅中,用锅铲迅速翻炒,使其均匀受热。
加调料:根据个人口味,加入适量的盐、酱油等调料,继续翻炒均匀。
炒熟:炒至青菜变软熟透,颜色鲜绿,即可关火。
盛盘:将炒好的青菜盛入盘中,即可享用。
这个炒青菜的算法就是一个简单的例子。它描述了一系列步骤,按照特定的顺序来完成一个任务。
而我们平时常见的冒泡排序(达成排序的目的)和二分查找(达成找到目标数据的目的)都是常见的算法。
解决问题的方法有好有坏,既然算法是用来解决问题的,那么就会引来一个问题,我们如何衡量一个算法的好坏?
1.时间复杂度
时间复杂度理论上来说是一个算法执行完所需要的时间,但是在实际情况下有两个弊端:1.一个算法执行所需要的时间必须是上机测试才能知道。2.执行的时间也和电脑硬件的性能有很大关系。所以,我们使用一个算法所花费的时间与其中语句的执行次数成正比例来定义时间复杂度。
举个例子
// 请计算一下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);
}
NN + 2N + 10次,我们在计算时间复杂度时只需要大概计算一下就可以,也就是找出影响最大的一项,显而易见,影响最大的就是N方
大O阶表示法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
而且时间复杂度要考虑最坏的情况,也就是按照最坏情况来表达,只有将预期放低,所遇到的一切皆为惊喜。
e.g.1
// 计算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)
e.g.2
// 计算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);
}
O(M + N)
e.g.3
// 计算Func4的时间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
O(1)
e.g.4
// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );
最好1次,最坏N次
O(N)
e.g.5
// 计算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 - 1)次,再比(n - 2)次,最后比1次。
最好1次,最坏n(n-1) / 2次(这个需要看算法思想,所以告诉我们时间复杂度也不一定都是要靠看代码)
O(N)
e.g.6
// 计算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;
}
二分查找法:最好1次
最坏 x / 2 / 2 / 2 == 1
2的N次方 = x
所以N = log 2底 x
O(log N)
算法里log就是默认以2为底。
e,g,7
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}
单线递归,每次算一步
O(N)
e.g.8
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
这是一个斐波那契数列的递归
O(n的平方)
2.空间复杂度
函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因
此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
// 计算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(1)
e.g.2
// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前n项
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;
}
用动态内存额外开辟了一个数组,所以是O(N)
二维数组可能是O(N的平方)
e.g.3
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}
递归的空间复杂度来看递归深度,一共调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N)。
拓展:斐波那契数列的空间复杂度
这是一个简图,说明函数栈帧是可以重复利用的,比如斐波那契数列在走完一路之后,遇到返回值了,再开始走旁边的岔路,而且同等深度的空间可以重复利用。
所以空间复杂度也是O(N)。
三、总结
总结:后期博主要开始主要学习数据结构和算法了!希望大家继续多多支持!本人还是个数据结构小白,后续会出一期栈帧的文章!大家多多指教!