【数据结构陈越版笔记】第1章 概论

我最近准备以陈姥姥的数据结构教材为蓝本重新学一下数据结构,写一下读书笔记

第1章 概论

1.1 引子

概论中首先描述了,数据结构的定义没有具体的定义,初学者可以不用管这个定义的问题,但是我理解的和维基百科的说法是一样的“数据结构是计算机中存储、组织数据的方式。通常情况下,精心选择的数据结构可以带来最优效率的算法。”
在这个笔记中,我的代码均使用纯C语言。
然后做了两段代码的对比,以此来说明算法运行时间(即时间复杂度)的重要性,先执行循环的,输入N为9999999

#include <stdio.h>// 循环打印1到N的全部整数
void CirPrintN(int N)
{int i = 0;for(i = 1; i<=N; i++){printf("%d\n", i);}
}
// 递归打印1到N的全部整数
void RecPrintN(int N)
{if(N>0){RecPrintN(N-1);printf("%d\n", N);}
}int main()
{int N = 0;scanf("%d", &N);CirPrintN(N);return 0;
}

循环版本的运行结果:

可以看到,循环版本是可以正常打印的
递归的版本出现异常,原因是它每一次递归都要保存当前的状态到递归调用栈中,所以它需要占用大量的内存空间,一旦数据量大,内存空间就不足了

然后又对比了秦九韶算法和普通的正常一次循环遍历的多项式求和算法(即求 f ( x ) = a 0 + a 1 x + a 2 x 2 + . . . + a n x n f(x)=a_{0}+a_{1}x+a_{2}x^{2}+...+a_{n}x^{n} f(x)=a0+a1x+a2x2+...+anxn

秦九韶算法是这样的,对于如下的多项式:
f ( x ) = a 0 + a 1 x + a 2 x 2 + . . . + a n − 1 x n − 1 + a n x n = a 0 + x ( a 1 + a 2 x + . . . + + a n − 1 x n − 2 + a n x n − 1 ) = a 0 + x ( a 1 + x ( a 2 + . . . + a n − 1 x n − 3 + a n x n − 2 ) ) = . . . = a 0 + x ( a 1 + x ( a 2 + . . . + x ( a n − 1 + a n x ) . . . ) ) f(x)=a_{0}+a_{1}x+a_{2}x^{2}+...+a_{n-1}x^{n-1}+a_{n}x^{n}\newline =a_{0}+x(a_{1}+a_{2}x+...++a_{n-1}x^{n-2}+a_{n}x^{n-1})\newline =a_{0}+x(a_{1}+x(a_{2}+...+a_{n-1}x^{n-3}+a_{n}x^{n-2}))\newline =...\newline =a_{0}+x(a_{1}+x(a_{2}+...+x(a_{n-1}+a_{n}x)...)) f(x)=a0+a1x+a2x2+...+an1xn1+anxn=a0+x(a1+a2x+...++an1xn2+anxn1)=a0+x(a1+x(a2+...+an1xn3+anxn2))=...=a0+x(a1+x(a2+...+x(an1+anx)...))

也就是说,我们可以从括号里算到括号外
然后下面对比这两种算法涉及到了C语言的time.h头文件
C语言的 time.h 头文件提供了与时间相关的函数和类型定义。这个头文件常用于获取当前时间、日期,或者进行时间的测量和转换。

clock_t 类型
clock_t 是一个数据类型,通常用于表示“时钟”或“处理器时间”的度量单位。它通常是一个长整型(long 或 long long)的别名,但具体的大小和表示方式可能依赖于系统和编译器。
在 time.h 中,clock_t 类型的值通常由 clock() 函数返回,该函数返回程序执行到该点为止的CPU时间(以“时钟滴答”为单位)。这个值可以用来测量代码段的执行时间。
clock() 函数
clock() 函数返回一个 clock_t 类型的值,表示从程序启动开始到 clock() 被调用时的CPU时间。这个函数通常用于性能分析和基准测试。
CLK_TCK(或 CLOCKS_PER_SEC)
CLK_TCK 或 CLOCKS_PER_SEC 是一个宏定义,它表示 clock() 函数返回的时钟滴答数每秒的个数。换句话说,它定义了 clock_t 类型的值与实际秒数之间的转换因子。用这个宏来将 clock() 的返回值转换为秒数。

下面就是对比运行时间的代码,其中,这里面的*f是函数指针的意思,在C语言中,函数指针是一个特殊的指针,它指向一个函数的入口地址。函数指针可以用来调用函数,或者作为其它函数的参数。

#include <stdio.h>
#include <time.h>
#include <math.h>clock_t start = 0;     //开始时间
clock_t stop = 0;      //结束时间
double duration = 0.0; //算法一共运行了多长时间#define MAXN 10  // 多项式最大项数,即多项式阶数+1
#define MAXK 1e7 // 被测函数最大重复调用次数// n为多项式的项数,a数组存储的是多项式各系数
//普通的循环法求多项式的和
double f1(int n, double a[], double x)
{int i = 0;double p = a[0];for (i = 1; i <= n; i++){p += (a[i] * pow(x, i));}return p;
}//秦九韶法求多项式的和
double f2(int n, double a[], double x)
{int i = 0;double p = a[n];for (i = n; i > 0; i--){p = a[i - 1] + x * p; //从最里面的括号开始算}return p;
}// 此函数用于测试被测函数*f,并且根据case_n输出相应的结果
// case_n是输出的函数编号:1代表函数f1;2代表函数f2
void run(double (*f)(int, double*, double), double a[], int case_n)
{int i = 0;start = clock();//重复调用函数以获得充分多的时钟打点数for (i = 0; i < MAXK; i++) // 调用MAXK次{(*f)(MAXN - 1, a, 1.1);}stop = clock();duration = ((double)(stop - start)) / CLK_TCK; // 转换为秒数printf("ticks%d= %f \n", case_n, (double)(stop - start));printf("duration%d = % 6.2e \n", case_n, duration);
}int main()
{int i = 0;double a[MAXN];for (i = 0; i < MAXN; i++){a[i] = (double)i;}run(f1, a, 1);run(f2, a, 2);return 0;
}

运行结果:

传统方法是1.39s,秦九韶算法是 2.01 × 1 0 − 1 2.01\times 10^{-1} 2.01×101秒,后者更快

这几个引子目的就是为了说明精心选择的数据结构可以带来最优效率的算法。

1.2 数据结构

1.2.1 定义

陈姥姥的书里讲的定义挺通俗易懂的,我这里直接把严蔚敏书的定义拿过来

  • 数据:数据是对客观事物的符号表示,在计算机科学中是指所有能输入到计算机中并被计算机程序处理的符号的总称。
  • 数据元素:数据元素是数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理。
  • 数据对象:数据对象是性质相同的数据元素的集合,是数据的一个子集。例如,整数数据对象是集合 N = { 0 , ± 1 , ± 2 , ⋯ } N=\{0, \pm 1, \pm 2, \cdots\} N={0,±1,±2,}
  • 数据结构:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。

通常有4类基本结构“

  1. 集合:结构中的元素之间除了”同属于一个集合“的关系外,别无其他关系;
  2. 线性结构:结构中的数据元素直接存在一个对一个的关系;
  3. 树形结构:结构中的数据元素直接存在一个对多个的关系;
  4. 图状结构或网状结构:结构中的数据元素之间存在多个对多个的关系。

    数据结构在计算机中的表示(又称映像)称为数据的物理结构,又称存储结构

1.2.2 抽象数据类型

抽象数据类型就是对数据类型的描述,也就是抽象(概括),比如我们要写一个矩阵的数据类型,它的抽象数据类型定义是:
(1)类型名称:矩阵(Matrix)
(2)数据对象集:一个 m × n m\times n m×n的矩阵 A m × n = ( a i j ) ( i = 1 , . . . , m , j = 1 , . . . , n ) A_{m\times n}=(a_{ij})(i=1, ..., m, j=1, ..., n) Am×n=(aij)(i=1,...,m,j=1,...,n) m × n m\times n m×n个三元组<a, i, j>构成,其中 a a a是矩阵元素的值, i i i是元素所在的行号, j j j是元素所在的列号。
(3)操作集:就是列出矩阵都有什么基本运算(操作)

  1. Matrix Create(int M, int N); //返回一个MxN的空矩阵
  2. int GetMaxRow(Matrix A); //返回矩阵A的总行数

……
等等
操作集是忽略操作的代码实现细节的,只需要关注其有什么功能即可。

1.3 算法

1.3.1 定义

一般而言,算法是一个有限指令集,它接受一些输入(有些情况下不需要输入),产生输出,并一定在有限步骤之后终止。

1.3.2 算法复杂度

  1. 空间复杂度 S ( n ) S(n) S(n)——根据算法写成的程序在执行时占用存储单元的长度。这个长度往往与输入数据的规模 n n n有关。空间复杂度过高的算法可能导致使用的内存超出限制而造成程序非正常中断。
  2. 时间复杂度 T ( n ) T(n) T(n)———根据算法写成的程序在执行时耗费时间的长度。这个长度往往也与输入的规模 n n n有关。时间复杂度过高的低效算法可能导致我们在有生之年都等不到运行结果。

刚才两个例子就说明了算法时间复杂度的问题。

在分析一般算法的效率时,我们经常关注下面两种复杂度:
(1)最坏情况复杂度 T w o r s t ( n ) T_{worst}(n) Tworst(n)
(2)平均复杂度 T a v g ( n ) T_{avg}(n) Tavg(n)

T w o r s t ( n ) T_{worst}(n) Tworst(n)分析比对 T a v g ( n ) T_{avg}(n) Tavg(n)分析容易,我们一般分析最坏复杂度。

1.3.3 渐进表示法

渐进表示法是我们分析复杂度的主要表示法,书中提到的上界函数下界函数暂时不用考虑,用大写字母 O O O表示渐进的复杂度,一个语句的频度是指该语句在算法中被重复执行的次数。算法中所有语句的频度之和记为 T ( n ) T(n) T(n),它是该算法问题规模 n n n的函数,算法的时间复杂度主要分析 T ( n ) T(n) T(n)的数量级,也就是分析 T ( n ) T(n) T(n)与哪些初等的简单函数为同阶无穷大。通常采用算法中基本运算的频度 f ( n ) f(n) f(n)来分析算法的时间复杂度。因此,算法的时间复杂度记为:
T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))

在微积分中,我们学过,当 n → ∞ n\to \infty n时,有如下的不等式:
ln n < n a < b n < n ! < n n ( a > 0 , b > 1 ) \text{ln}n<n^{a}<b^{n}<n!<n^{n}(a>0,b>1) lnn<na<bn<n!<nn(a>0,b>1)
最后展开详细说就是:
O ( 1 ) < O ( log ⁡ 2 n ) < O ( n ) < O ( n log ⁡ 2 n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)<O\left(\log _{2} n\right)<O(n)<O\left(n \log _{2} n\right)<O\left(n^{2}\right)<O\left(n^{3}\right)<O\left(2^{n}\right)<O(n!)<O\left(n^{n}\right) O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
正好也对应着书中的常用函数增长曲线

对于做渐进时间复杂度分析时,有5个规律:
(1)若两段算法分别有复杂度 T 1 ( n ) = O ( f 1 ( n ) ) T_{1}(n)=O(f_{1}(n)) T1(n)=O(f1(n)) T 2 ( n ) = O ( f 2 ( n ) ) T_{2}(n)=O(f_{2}(n)) T2(n)=O(f2(n)),那么:两段算法串联在一起的时间复杂度为 max O ( f 1 ( n ) ) , O ( f 2 ( n ) ) \text{max}{O(f_{1}(n)), O(f_{2}(n))} maxO(f1(n)),O(f2(n)),也就是取时间复杂度最大的;若两段算法嵌套在一起(循环里嵌套循环那种):则时间复杂度为 O ( f 1 ( n ) f 2 ( n ) ) O(f_{1}(n)f_{2}(n)) O(f1(n)f2(n))(相乘)
比如,有如下代码:

int i = 1, k = 0;
while (i <= n - 1)
{k += 10 * i;i++;
}

对于上述代码来说,其while循环执行了n-1次,则其时间复杂度为 O ( n − 1 ) O(n-1) O(n1)又根据同阶无穷大的原则,最终(1)的时间复杂度为 O ( n ) O(n) O(n)
再比如,有如下代码:

int i=0,k = 0,j=0;
for (i = 1; i <= n; i++)
{
for (j = i; k <= n; j++){k++;}
}

上述代码共有两层for循环,外层循环执行了n次,而外层循环每执行一次里层循环都执行n次,这样外层循环执行次数与内层循环执行次数相乘即为答案,故它的时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)
(2)若 T ( n ) T(n) T(n)是关于 n n n k k k阶多项式,那么 T ( n ) = O ( n k ) T(n)=O(n^{k}) T(n)=O(nk)
(3)一个for循环的时间复杂度等于循环次数乘以循环体代码的复杂度。例如这个循环的复杂度是 O ( n ) O(n) O(n)

for(int i = 0; i< N;i++)
{x = y*x+z;k++;
}

(4)若干层嵌套循环的时间复杂度等于各层循环次数的乘积再乘以循环体代码的复杂度,例如下列2层嵌套循环的复杂度是 O ( n 2 ) O(n^{2}) O(n2)

for(int i = 0; i< N;i++)
{for(int j =0; j< N;j++){x = y*x+z;k++;}
}

(5)if-else结构的复杂度取决于if的条件判断复杂度和两个分支部分的复杂度,总体复杂度取三者中最大,即对结构:
if(P1):
P2;
else:
P3;
总复杂度为 O ( max ( f 1 , f 2 , f 3 ) ) O(\text{max}(f_{1},f_{2},f_{3})) O(max(f1,f2,f3)),其中 f i , i = 1 , 2 , 3 f_{i},i=1,2,3 fi,i=1,2,3代表 P i P_{i} Pi的复杂度。

1.4 应用实例:最大子列和问题

这个问题与LeetCode53最大子数组和是类似的,只是在关于最终求和是负数的问题上处理的不一样,陈姥姥的书中是这样描述问题的,给定 n n n个整数的序列 a 1 , a 2 , . . . , a n a_{1}, a_{2}, ..., a_{n} a1,a2,...,an,求函数 f ( i , j ) = max ⁡ { 0 , ∑ k = i j a k } f(i, j)=\max \left\{0, \sum\limits_{k=i}^{j} a_{k}\right\} f(i,j)=max{0,k=ijak},也就是,我们要寻找的是具有最大和的一段连续的子列,并且返回它的和。如果这个最大和是负数,那么我们取0为最终答案(LeetCode则在此种情况下返回负数最大和)。例如给定序列{-2, 11, -4, 13, -5, 2},其最大子列为{11, -4, 13},和为20.

1.4.1 暴力法

暴力法就是穷举所有子列的和,从中找出最大值,代码如下:

#include <stdio.h>//暴力法
int MaxSubseqSum1(int List[], int N)
{int ThisSum = 0; //当前子列的和int MaxSum = 0; //最大子列和,默认赋值为0,如果和为负数,就只能返回0//i是子列左端位置for (int i = 0; i < N; i++){//j是子列右端位置for (int j = i; j < N; j++){ThisSum = 0;// 把子列和(从List[i]加到List[j])加一起for (int k = i; k <= j; k++){ThisSum += List[k];}// 如果当前和超过之前的最大和,则最大和赋值成这个if (ThisSum > MaxSum){MaxSum = ThisSum;}}}return MaxSum;
}int main()
{int a[6] = { -2, 11, -4, 13, -5, -2 };printf("%d\n", MaxSubseqSum1(a, 6));int b[3] = { -2, -4, -5};printf("%d\n", MaxSubseqSum1(b, 3));return 0;
}

暴力法三层for循环,时间复杂度更是看一眼多一眼就会爆炸的 O ( n 3 ) O(n^{3}) O(n3),其实这么写暴力法完全没必要,主要是从List[i]加到List[j]的过程没必要,我们返回的是最大子列和而不是返回最大子列的所有的元素,所以,我们完全可以从j那个循环下手,直接让ThisSum+=List[j],然后再比较大小,这样就算j的继续遍历到下一个使得ThisSum和变小了,我们也不用担心,因为根本不会进入if条件导致MaxSum赋值为这个变小了的ThisSum,我们求的只是子列和,不是子列的元素,这一点要记住,所以这个暴力法还能优化成一个时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)的暴力法,即:

#include <stdio.h>//暴力法2
int MaxSubseqSum2(int List[], int N)
{int ThisSum = 0; //当前子列的和int MaxSum = 0; //最大子列和,默认赋值为0,如果和为负数,就只能返回0//i是子列左端位置for (int i = 0; i < N; i++){ThisSum = 0; // ThisSum清零的工作就放到了j这个循环的外层//j是子列右端位置for (int j = i; j < N; j++){ThisSum += List[j];// 如果当前和超过之前的最大和,则最大和赋值成这个if (ThisSum > MaxSum){MaxSum = ThisSum;}}}return MaxSum;
}int main()
{int a[6] = { -2, 11, -4, 13, -5, -2 };printf("%d\n", MaxSubseqSum2(a, 6));int b[3] = { -2, -4, -5};printf("%d\n", MaxSubseqSum2(b, 3));return 0;
}

但是这仍然不是时间复杂度最好的算法。

1.4.2 分治法

顾名思义,分而治之法的基本思路就是将问题拆成若干小问题,分别解决后再将结果合起来,用递归实现非常方便。我们可以把这个问题的原始序列一分为二,那么最大子列或者在左半边或者在右半边,或者是横跨中分线的一段。于是我们得到了这个算法的步骤:

  1. 将序列从中分为左右两个子序列;
  2. 递归(或循环)求得两子列的最大和 S 左 S_{左} S S 右 S_{右} S
  3. 从中分点分头向左、右两边扫描,找出跨过分界线的最大子列和 S 中 S_{中} S
  4. S max = max { S 左 , S 右 , S 中 } S_{\text{max}}=\text{max}\{S_{左}, S_{右}, S_{中}\} Smax=max{S,S,S}

这个分治法用循环去解太难想了,我根据它的描述和书中的代码,写了一段代码,加了详细的注释:

// 比较三个数中最大数的宏定义
#define MAX3(A, B, C) (( A > B ? A : B) > C) ? ( A > B ? A : B) : C// 分治法递归求最大子列和
int DivideAndConquer(int* List, int left, int right)
{int MaxLeftSum = INT_MIN; // 左子列的最大和int MaxRightSum = INT_MIN; // 右子列的最大和int MaxLeftBorderSum = INT_MIN; //跨越中点的子列的左侧的和int MaxRightBorderSum = INT_MIN; //跨越中点的子列的右侧的和int LeftBorderSum = 0; //跨越中点的子列的左侧的和(不一定是最大的)int RightBorderSum = 0; //跨越中点的子列的右侧的和int middle = 0; //分治法求分界点的变量s// left与right重合时,递归停止,也就是子列只有一个数字// 这是最小的子列,其和就是这一个元素,如果它的和// 也就是这一个元素为负数或者0,则应该返回0(根据题意)// 如果是LeetCode53,则应该直接返回List[left],不需要加判断条件// 因为LeetCode53是需要对比负数和的if(left == right){if(List[left] > 0){return List[left];}else{return 0;}}// 求解中点,向右移动一位相当于除2middle = (right + left)>>1;  // 此处也可以等价成(right - left)/2 + left,但是这样写会超时//递归求解左子列和右子列的最大和MaxLeftSum = DivideAndConquer(List, left, middle);MaxRightSum = DivideAndConquer(List, middle + 1, right);//求跨越中点的子列的最大和MaxLeftBorderSum = INT_MIN; //每次求和之前,要将最大值变为无穷小,方便比较LeftBorderSum = 0;//找跨越中点的子列的左侧的最大和(从中点向左遍历)for(int i = middle; i>=left; i--){LeftBorderSum += List[i];if(LeftBorderSum > MaxLeftBorderSum){MaxLeftBorderSum = LeftBorderSum;}}//找跨越中点的子列的右侧的最大和(从中点向右遍历)MaxRightBorderSum = INT_MIN; //每次求和之前,要将最大值变为无穷小,方便比较RightBorderSum = 0;for(int i = middle + 1; i<=right; i++){RightBorderSum += List[i];if(RightBorderSum > MaxRightBorderSum){MaxRightBorderSum = RightBorderSum;}}// 返回左子列,跨越中点的子列和右子列三者中的最大值return MAX3(MaxLeftSum, MaxLeftBorderSum + MaxRightBorderSum, MaxRightSum);
}
int maxSubArray(int* List, int N) {return DivideAndConquer(List, 0, N-1);
}

按分治法所说,这是不断地二分求解子问题,相当于是每次都分为 1 2 \frac{1}{2} 21,那么假设这个算法的整体的时间复杂度为 T ( n ) T(n) T(n),则DivideAndConquer函数中递归进行分治的复杂度为 2 T ( n 2 ) 2T(\frac{n}{2}) 2T(2n),因为我们是对左右两个子列不断二分求解,相当于解决了2个长度减半的子问题(就是那两个递归地求左子列最大和以及右子列最大和的过程,这相当于以 n 2 \frac{n}{2} 2n的规模再调用函数,也就是递归二分的过程,所以求单侧最大和的复杂度是 T ( n 2 ) T(\frac{n}{2}) T(2n)),求跨越分界线的最大子列和时,相当于求解了两个最坏复杂度为 O ( n 2 ) O(\frac{n}{2}) O(2n)的问题,也就是最坏时间复杂度为 O ( n 2 + n 2 = n ) = O ( n ) O(\frac{n}{2}+\frac{n}{2}=n)=O(n) O(2n+2n=n)=O(n)的问题,那么它整体的时间复杂度为:
T ( n ) = 2 T ( n 2 ) + O ( n ) = 2 [ 2 T ( n 2 2 ) + O ( n 2 ) ] + O ( n ) = 2 2 T ( n 2 2 ) + 2 O ( n ) = . . . = 2 k T ( n 2 k ) + k O ( n ) T(n)=2T(\frac{n}{2})+O(n)\newline =2[2T(\frac{\frac{n}{2}}{2})+O(\frac{n}{2})]+O(n)\newline =2^{2}T(\frac{n}{2^{2}})+2O(n)\newline =...\newline =2^{k}T(\frac{n}{2^{k}})+kO(n) T(n)=2T(2n)+O(n)=2[2T(22n)+O(2n)]+O(n)=22T(22n)+2O(n)=...=2kT(2kn)+kO(n)
当我们不断地递归,直到递归到问题规模为1的时候,即 n 2 k = 1 \frac{n}{2^{k}}=1 2kn=1, n = 2 k n=2^{k} n=2k k = log 2 n k=\text{log}_{2}{n} k=log2n,就能得到 T ( n ) = 2 k T ( n 2 k ) + 2 O ( n ) = 2 log 2 n T ( 1 ) + log 2 n × O ( n ) = n T ( 1 ) + O ( n ) log 2 n = n × 1 + O ( n log 2 n ) = O ( n + n log 2 n ) = O ( n log 2 n ) T(n)=2^{k}T(\frac{n}{2^{k}})+2O(n)=2^{\text{log}_{2}{n}}T(1)+\text{log}_{2}{n}\times O(n)=nT(1)+O(n)\text{log}_{2}{n}=n\times1+O(n\text{log}_{2}{n})=O(n+n\text{log}_{2}{n})=O(n\text{log}_{2}{n}) T(n)=2kT(2kn)+2O(n)=2log2nT(1)+log2n×O(n)=nT(1)+O(n)log2n=n×1+O(nlog2n)=O(n+nlog2n)=O(nlog2n),所以这种算法的时间复杂度为 O ( n log 2 n ) O(n\text{log}_{2}{n}) O(nlog2n)

1.4.3 在线处理(动态规划)

“在线”的意思是指每输入一个数据就进行即时处理,得到结果是对于当前已经读入的所有数据都成立的解,即再任何一个地方终止输入,算法都能正确给出当前的解。
前面所给出的3种算法都必须等所有的 N N N个整数都读入并存储后才可以进行,而在线处理的方法甚至无须存储输入序列就可以得到任何时刻的最大子列和。
该算法的核心思想是基于下面的事实:如果整数序列 { a 1 , a 2 , ⋯ , a n } \left\{a_{1}, a_{2}, \cdots, a_{n}\right\} {a1,a2,,an}的最大子列和是 { a i , a i + 1 , ⋯ , a j } \left\{a_{i}, a_{i+1}, \cdots, a_{j}\right\} {ai,ai+1,,aj},那么必定有 ∑ k = i l a k ⩾ 0 \sum\limits_{k=i}^{l} a_{k} \geqslant 0 k=ilak0对任意 i ⩽ l ⩽ j i \leqslant l \leqslant j ilj成立,因此,一旦发现当前子列和为负,则可以重新开始考察一个新的子列。代码如下:

int maxSubArray(int* nums, int numsSize){int result=0;//最开始假定最大值为0,因为这个题目要求的是负数和算为0,如果和LeetCode53一样需要负数和,此处应设置为INT_MINint count =0;//子数组的求和结果for(int i=0;i<numsSize;i++){count = count + nums[i];//count大于假定的最大值,就让假定的最大值等于countif(count > result){result = count;}//加和小于等于0,则其不是最大连续子序列,让count从0开始加//如果加和变成负数,说明从nums[i]开始向前到nums[0]的数无论怎么取连续子数组都只能小于等于result//result记录的是nums[i]之前的数字的最大连续子数组的和//所以,就没必要再回到前面去找加和了,直接从nums[i]向后加和对比//如果从nums[i]开始到最后的加和中出现了加和大于nums[i]之前的最大连续子数组的和result//那就让result赋值为nums[i]后面的最大连续子数组的和的值//这样就找到了最大连续子数组的和if(count<0){count =0;}}return result;
}

该算法只有一层规模为 n n n的for循环,故其时间复杂度为 O ( n ) O(n) O(n),这个例子是告诉大家,解决同一个问题,不同的算法会有很大的差别,让计算机记住一些中间的计算结果,可以避免重复计算。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/837536.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

全面了解 Swagger 导出功能的使用方式

Swagger 是一个强大的平台&#xff0c;专门用于开发、构建和记录 RESTful Web 接口。通过其提供的交互式用户界面&#xff0c;开发人员能够轻松且迅速地创建和测试 API。Swagger 还允许用户以多种格式&#xff0c;包括 JSON 和 Markdown&#xff0c;导出 API 文档。选择 JSON 格…

人工神经网络(科普)

人工神经网络&#xff08;Artificial Neural Network&#xff0c;即ANN &#xff09;&#xff0c;是20世纪80 年代以来人工智能领域兴起的研究热点。它从信息处理角度对人脑神经元网络进行抽象&#xff0c; 建立某种简单模型&#xff0c;按不同的连接方式组成不同的网络。在工程…

MySQL中的索引失效问题

索引失效的情况 这是正常查询情况&#xff0c;满足最左前缀&#xff0c;先查有先度高的索引。 1. 注意这里最后一种情况&#xff0c;这里和上面只查询 name 小米科技 的命中情况一样。说明索引部分丢失&#xff01; 2. 这里第二条sql中的&#xff0c;status > 1 就是范围查…

什么品牌洗地机最好?怎么选?2024家用洗地机推荐攻略

随着科技的不断发展&#xff0c;家用洗地机已经成为人们家庭清洁任务重非常重要的辅助工具。家用洗地机集吸尘、扫地、拖地等功能于一体&#xff0c;通过高速旋转的滚刷和强力的吸力&#xff0c;将地面上的污渍、细菌和毛发等吸入污水箱&#xff0c;从而达到清洁地面的目的。但…

软件验收测试包括哪些类型

在软件开发过程中&#xff0c;验收测试是一个至关重要的环节&#xff0c;它确保了软件的质量、功能性和用户体验符合预期。验收测试主要关注于软件是否满足用户需求和业务目标&#xff0c;从而确保软件能够顺利交付并投入使用。本文将介绍软件验收测试的主要类型及其关键要素。…

扩展van Emde Boas树以支持卫星数据:设计与实现

扩展van Emde Boas树以支持卫星数据&#xff1a;设计与实现 1. 引言2. vEB树的基本概念3. 支持卫星数据的vEB树设计3.1 数据结构的扩展3.2 操作的修改3.3 卫星数据的存储和检索 4. 详细设计和实现4.1 定义卫星数据结构体4.2 修改vEB树节点结构4.3 插入操作的伪代码4.4 C语言实现…

声纹识别的对抗与防御

随着机器学习理论和方法的发展, 出现了用于模仿特定说话人语音的深度伪造、针对语音识别和声纹识别的对抗样本, 它们都为破坏语音载体的可信性和安全性提供了具体手段, 进而对各自应用场景的信息安全构成了挑战。 深度伪造是利用生成式对抗网络等方法, 通过构建特定的模型, 产生…

【爬虫之scrapy框架——尚硅谷(学习笔记one)--基本步骤和原理+爬取当当网(基本步骤)】

爬虫之scrapy框架——基本原理和步骤爬取当当网&#xff08;基本步骤&#xff09; 下载scrapy框架创建项目&#xff08;项目文件夹不能使用数字开头&#xff0c;不能包含汉字&#xff09;创建爬虫文件&#xff08;1&#xff09;第一步&#xff1a;先进入到spiders文件中&#x…

Python多任务

进程 1. 进程的概念 一个正在运行的程序或者软件就是一个进程&#xff0c;它是操作系统进行资源分配的基本单位&#xff0c;也就是说每启动一个进程&#xff0c;操作系统都会给其分配一定的运行资源(内存资源)保证进程的运行。 比如:现实生活中的公司可以理解成是一个进程&a…

OpenAI 发布新款大型语言模型 GPT-4o,带大家了解最新ChatGPT动态。

OpenAI 发布新款大型语言模型 GPT-4o 昨日OpenAI 举办了一场线上活动&#xff0c;正式发布了其最新研发的 AI 模型 GPT-4o&#xff0c;并详细介绍了该模型的强大功能和未来发展规划。此次发布标志着 AI 技术的重大突破&#xff0c;为用户提供了更加便捷、高效的 AI 工具&#…

一张表搞定物业巡检?没错,就是这么神奇!

在车水马龙的城市中&#xff0c;高楼大厦鳞次栉比&#xff0c;它们不仅为城市形成一道风景线&#xff0c;也是我们日常工作与生活的家园。然而&#xff0c;在这背后&#xff0c;有一群默默付出的物业工作人员&#xff0c;用责任和担当守护着我们的安全与舒适。而在物业日常工作…

STM32IAP学习笔记

单片机不同的程序下载方式 ICP ICP是指在电路中编程。使用厂家配套的软件或仿真器进行程序烧录&#xff0c;目前主流的有JTAG接口和SWD接口&#xff0c;常用的烧录工具为J-Link、ST-Link等。在程序开发阶段&#xff0c;通常在连接下载器的情况下直接使用编程软件进行程序下载调…

护照OCR识别接口如何对接

护照OCR识别接口也叫护照文字识别OCR,指的是传入护照照片&#xff0c;精准识别静态护照图像上的文字信息&#xff0c;包括姓名、签发地点、签发机关、护照号码、签发日期等信息。那么护照文字识别OCR接口如何对接呢&#xff1f; 首先我们找到一家有护照OCR识别接口的服务商数脉…

【万字面试题】Redis

文章目录 常见面试题布隆过滤器原理和数据结构&#xff1a;特点和应用场景&#xff1a;缺点和注意事项&#xff1a;在python中使用布隆过滤器 三种数据删除策略LRU (Least Recently Used)工作原理&#xff1a;应用场景&#xff1a; LFU (Least Frequently Used)工作原理&#x…

Navicat16小白式安装和激活详解《简单》

简介&#xff1a; Navicat 是一款强大的数据库管理和开发工具&#xff0c;它支持多种数据库系统&#xff0c;包括 MySQL、MariaDB、MongoDB、SQL Server、Oracle、PostgreSQL 以及 SQLite。Navicat 提供了图形界面&#xff08;GUI&#xff09;来简化数据库的管理、操作和维护。…

柔性数组+结构体类型转换

柔性数组&#xff1a;在结构体中声明的时候仅作为占位符&#xff0c;好处是地址是连续的 强制类型转换&#xff1a;可用于通信双方进行信息交流 #include <iostream> #include <string.h>struct DataWater {int count;float size;char buf[0]; }; // dbuf相当于是…

MYSQL中的DQL

语法&#xff1a; select 字段列表 from 表名列表 where 条件列表 group by 分组字段列表 having 分组后条件列表 order by 排序字段 limit 分页参数 条件查询 语法&#xff1a; 查询多个字段&#xff1a;select 字段1&#xff0c;字段2 from表名 查询所有字段&#xff1a…

“打工搬砖记”中首页的功能实现(一)

文章目录 打工搬砖记秒薪的计算文字弹出动画根据时间数字变化小结 打工搬砖记 先来一个小程序首页预览图&#xff0c;首页较为复杂的也就是“秒薪”以及弹出文字的动画。 已上线小程序“打工人搬砖记”&#xff0c;进行预览观看。 秒薪的计算 秒薪计算公式&#xff1a;秒薪…

RPA的全新形态—Agent智能体:当机器人开始“听”话

随着人工智能技术的不断进步&#xff0c;RPA正迈向其全新形态——Agent智能体。想象一下&#xff0c;如果你的日常工作中有一个智能助手&#xff0c;它不仅能理解你的需求&#xff0c;还能自动帮你完成那些繁琐的任务&#xff0c;这会是怎样的体验&#xff1f;这就是RPA技术正在…

从零创建一个vue2项目

标题从零创建一个vue2项目&#xff0c;项目中使用TensorFlow.js识别手写文字 npm切换到淘宝镜像 npm config set registry https://registry.npm.taobao.org安装vue/cli -g npm install -g vue/cli检查是否安装成功 vue -V创建项目 vue create 项目名安装TensorFlow npm …