数据结构前言

1. 什么是数据结构?


数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的 数据元素的集合。(该如何最佳的方式存储对应的数据,在内存当中对数据进行增删查改)

2.什么是算法?


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

3.算法的时间复杂度和空间复杂度


3.1 算法效率

3.1.1 如何衡量一个算法的好坏

如何衡量一个算法的好坏呢?比如对于以下斐波那契数列:

如何衡量一个算法的好坏呢?比如对于以下斐波那契数列:

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

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

3.1.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 执行的基本操作次数 :

F(N)= N ^ 2 + 2*N +10

当式子很长并且N很大的时候,F(N)就难以算出来,因此:

我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。(抓大头取最高阶数的项,取具有决定性结果的那一项)

所以此题的时间复杂度为:

O(N^2)

4.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;
}


数据结构前言_第1张图片

总共会调用N+1次,递归算法是多次调用的累加

所以答案为:O(N)

// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
 if(N < 3)
 return 1;
 
 return Fib(N-1) + Fib(N-2);
}

我们把具体fib的函数给展开

可以得到是以2的次方累加直到(N-1)数据结构前言_第2张图片

所以是O(2^N)

我们来做一道力扣题

面试题 17.04. 消失的数字 - 力扣(LeetCode)

数据结构前言_第3张图片

因为是顺序的,所以我们可以想到类似单身狗的问题,如果想知道单身狗怎么求解可以查看我上条博客如何用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;
}

你可能感兴趣的:(数据结构)