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

news2024/12/28 4:24:01

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

第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;
        }
    }
    // 求解中点,向右移动一位相当于除2
    middle = (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_MIN
    int count =0;//子数组的求和结果
    for(int i=0;i<numsSize;i++)
    {
        count = count + nums[i];
        //count大于假定的最大值,就让假定的最大值等于count
        if(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.coloradmin.cn/o/1663206.html

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

相关文章

[已解决]ModuleNotFoundError: No module named ‘tqdm‘

&#x1f60e; 作者介绍&#xff1a;我是程序员行者孙&#xff0c;一个热爱分享技术的制能工人。计算机本硕&#xff0c;人工制能研究生。公众号&#xff1a;AI Sun&#xff0c;视频号&#xff1a;AI-行者Sun &#x1f388; 本文专栏&#xff1a;本文收录于《AI实战中的各种bug…

如何批量删除多个不同路径的文件但又保留文件夹呢

首先&#xff0c;需要用到的这个工具&#xff1a; 度娘网盘 提取码&#xff1a;qwu2 蓝奏云 提取码&#xff1a;2r1z 1、我准备了三个文件夹&#xff08;实际操作的时候可能是上百个文件夹&#xff0c;无所谓&#xff09;&#xff0c;里面都放了两个图片 2、然后打开工具&am…

RustDesk 自建服务器部署和使用教程

RustDesk 是一个强大的开源远程桌面软件&#xff0c;是中国开发者的作品&#xff0c;它使用 Rust 编程语言构建&#xff0c;提供安全、高效、跨平台的远程访问体验。可以说是目前全球最火的开源远程桌面软件了&#xff0c;GitHub 星星数量达到了惊人的 64k&#xff01; 与 Team…

AIGC|将GPTBots与10000+主流软件连接,实现应用场景全覆盖

一、自动化工作流的无限可能&#xff0c;由AI带来 当前市场上存在许多自动化工作流工具&#xff0c;这些工具在很大程度上提升了人们的工作效率&#xff0c;为企业节省了大量时间和人力成本。然而&#xff0c;这些工具并非万能&#xff0c;它们在实际应用中仍存在一定的局限性…

如何在自己的服务器上快速搭建第一个网站(其一)

根据上篇文章相信很多人以及成功搭建服务器啦。今天我们讲下如何在自己的服务器快速搭建第一个网站的一些重要配置&#xff0c;以及搭建网站的必备环境。干货满满&#xff0c;希望大家能够关注点赞收藏。 我会不定期更新一些实用的工具&#xff0c;欢迎大家私信评论喔&#xf…

工作中使用Optional处理空指针异常

工作中使用Optional处理空指针异常 实体类以前对空指针的判断Optional处理空指针测试结果 实体类 package po;import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;import java.io.Serializable;Data AllArgsConstructor NoArgsConstruct…

kafka生产者消费者举例

文章目录 kafka介绍生产者消费者例子一、生产者二、消费者三、效果 KafkaTemplate KafkaListener kafka介绍 Kafka 是一款分布式流处理平台&#xff0c;它被设计用于高吞吐量、持久性、分布式的数据流处理。 Kafka 简介&#xff1a; Kafka 是一个高吞吐、分布式、基于发布 订阅…

NSS刷题

[SWPUCTF 2021 新生赛]jicao 类型&#xff1a;PHP、代码审计、RCE 主要知识点&#xff1a;json_decode()函数 json_decode()&#xff1a;对JSON字符串解码&#xff0c;转换为php变量 用法&#xff1a; <?php $json {"ctf":"web","question"…

2024年数维杯B题完整代码和思路论文讲解与分析

2024数维杯数学建模完整代码和成品论文已更新&#xff0c;获取↓↓↓↓↓ https://www.yuque.com/u42168770/qv6z0d/bgic2nbxs2h41pvt?singleDoc# 2024数维杯数学建模B题45页论文和代码已完成&#xff0c;代码为全部问题的代码 论文包括摘要、问题重述、问题分析、模型假设、…

怎么让电脑耳机和音响都有声音

电脑耳机音响不能同时用没声音怎么办 一般来说&#xff0c;重新开机后问题能够得到解决。右击“我的电脑”---“属性”---“硬件”---“设备管理器”&#xff0c;打开“声音、视频和游戏控制器”有无问题&#xff0c;即看前面有没有出现黄色的“”。 如果您的 电脑 耳机能正常…

SQL注入(sqli-labs第一关)

sqli-labs第一关 方法一&#xff1a;手工注入 来到第一关&#xff0c;图上说我们需要一个数字的参数 于是我们先手工注入?id1 and 11 跟?id1 and 12发现页面没有报错 每张截图上面页面中有select查询语句&#xff0c;这是我在第一关的源码中加上了echo "$sql ";…

基于UDP协议Python通信网络程序(服务器端+客户端)及通信协议在自动驾驶场景应用示例

一、UDP协议 UDP&#xff08;用户数据报协议&#xff09;是一种无连接的传输层协议&#xff0c;具有简单、高效的特点&#xff0c;适用于一些对数据可靠性要求不高的应用场景。UDP的主要特点包括无连接、不可靠和面向数据报。这意味着在发送数据之前不需要建立连接&#xff0c…

【Spring之依赖注入】2. Spring处理@Async导致的循环依赖失败问题

使用异步Async注解后导致的循环依赖失败详解 1 问题复现1.1 配置类1.2 定义Service1.3 定义Controller1.4 启动springboot报错 2.原因分析&#xff1a;看Async标记的bean注入时机2.1 循环依赖生成过程2.2 自检程序 doCreateBean方法 3.解决方案3.1 懒加载Lazy3.1.1 将Lazy写到A…

根据部门id删除该部门下的员工(事务)

application.properties&#xff1a; 或&#xff1a; application.yml&#xff1a; 新表&#xff1a; 日志对象类&#xff1a; 日志service类&#xff1a; 日志service接口&#xff1a; 日志mapper类&#xff1a; 部门service类&#xff1a; 员工mapper类&#xff1a;

某大型集团SAP数字化转型方案(95页PPT)

一、资料介绍 《某大型集团SAP数字化转型方案》是一份详尽的95页PPT资料&#xff0c;旨在为某大型集团提供一套全面而深入的SAP数字化转型方案。该方案紧密结合了集团的业务特点和发展需求&#xff0c;以SAP系统为核心&#xff0c;通过数字化技术的运用&#xff0c;实现业务流…

【redis】Redis五种常用数据类型和内部编码,以及对String字符串类型的总结

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如…

ESP32引脚入门指南(七):从理论到实践(IIC)

引言 IIC&#xff08;Inter-Integrated Circuit&#xff09;&#xff0c;又称为IC&#xff0c;是一种简单而高效的多主控器串行通信协议&#xff0c;常用于微控制器和各种外围设备之间的通信。在ESP32系列芯片中&#xff0c;IIC协议被广泛应用于连接各种传感器、存储器和其他支…

计算机网络实验1:交换机基本配置管理

实验目的和要求 安装Packer Tracer&#xff0c;了解Packer Tracer的基本操作掌握交换机基本命令集实验项目内容 认识Packet Tracer软件 交换机的基本配置与管理 交换机的端口配置与管理 交换机的端口聚合配置 交换机划分Vlan配置 实验环境 硬件&#xff1a;PC机&#x…

HTML5 + CSS3实现卖茶女与水果男的巅峰微信聊天对决,看完后笑一整天

记得之前看过一段卖茶女与水果男的聊天视频&#xff0c;当时觉得真有意思&#xff0c;竟然还可以这样热爱自己的事业。我就想&#xff0c;用HTML5 CSS3实现一下这个过程&#xff0c;锻炼了技术&#xff0c;也娱乐了开发人员&#xff0c;多有意思的一件事啊。 目录 1 实现思路…

Dbeaver连接一段时间不操作后断开的问题

右键数据库连接点击编辑连接点击初始化将连接保持改成60s