浙江大学数据结构陈越 第一讲 数据结构和算法

数据结构

在这里插入图片描述
数据结构是计算机科学中用来组织和存储数据的方式。它可以理解为一种组织数据的方式,能够有效地管理和操作数据,以及提供对数据进行存储、检索、更新和删除等操作的方法。常见的数据结构包括数组、链表、栈、队列、树和图等,它们各自适用于不同的应用场景,并且有着不同的特点和操作方式。数据结构的选择对于解决特定的计算问题非常重要,合适的数据结构能够提高算法的效率和性能。

讨论1.1对中等规模、大规模的图书摆放,你有什么更好的建议?

提出这个问题,实际上是想让大家思考,在考虑大规模数据存储的时候会遇到什么问题,以及如何根据功能(也就是关联的算法,最常见的就是插入、查找、删除)需要设计存储方式。
来自课件“讨论1.1对中等规模、大规模的图书摆放,你有什么更好的建议?”

在这里插入图片描述

解决问题方法的效率,跟数据的组织方式有关

在这里插入图片描述

解决问题方法的效率,跟空间的利用效率有关

时间复杂度描述了算法解决问题所需的时间量级,而空间复杂度描述了算法解决问题所需的内存空间量级。一个高效的算法应该在时间上尽可能快速地解决问题,并且在空间上尽可能节约内存资源。

有时候,时间复杂度和空间复杂度之间存在折衷关系。例如,某些算法可能通过使用更多的内存空间来降低时间复杂度,或者通过牺牲一定的时间性能来减少内存占用。在实际开发中,需要根据具体的应用场景和需求权衡时间和空间的利用效率。

因此,当评估解决问题方法的效率时,不仅要考虑算法的执行速度,还要考虑所需的内存空间量级。只有在时间和空间两方面都表现出较好的性能时,算法才能被视为高效的解决方法。

当涉及到时间复杂度和空间复杂度时,我们可以通过一个经典的例子来说明。让我们以计算斐波那契数列为例。

首先是使用递归的方法来计算斐波那契数列:

int fibonacci(int n) {
    if (n <= 1) {
        return n;
    } else {
        return fibonacci(n-1) + fibonacci(n-2);
    }
}

这个递归实现非常简洁,但是在时间复杂度和空间复杂度上存在一些问题。时间复杂度是O(2^n),因为每次调用都会导致两个更小规模的调用,这使得计算量呈指数级增长。同时,递归调用会占用大量的栈空间,因此空间复杂度也很高。

为了提高效率,我们可以使用迭代的方式来计算斐波那契数列:

int fibonacci(int n) {
    int a = 0, b = 1, next, i;
    if (n == 0)
        return a;
    for (i = 2; i <= n; i++) {
        next = a + b;
        a = b;
        b = next;
    }
    return b;
}

这种迭代的方法时间复杂度为O(n),空间复杂度为O(1)。相比之下,迭代方法在时间和空间上都表现更优秀。

因此,这个例子很好地展示了算法的时间复杂度和空间复杂度对于效率的影响,以及如何通过不同的方法来提高算法的效率。

在这里插入图片描述

解决问题的方法效率,跟算法的巧妙程度有关

下面以排序算法为例,展示不同算法的巧妙程度对效率的影响。

假设我们需要对一个整数数组进行升序排序。

首先,我们来看一种简单但效率较低的冒泡排序算法:

void bubbleSort(int arr[], int size) {
    for (int i = 0; i < size - 1; i++) {
        for (int j = 0; j < size - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

冒泡排序通过不断交换相邻元素的位置来将最大元素逐步冒泡到末尾,时间复杂度为O(n^2)。虽然这个算法实现简单,但效率相对较低,特别是当数组规模较大时。

接下来,我们来看一种更巧妙、效率更高的快速排序算法:

int partition(int arr[], int low, int high) {
    int pivot = arr[high];
    int i = low - 1;
    
    for (int j = low; j <= high - 1; j++) {
        if (arr[j] < pivot) {
            i++;
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    int temp = arr[i + 1];
    arr[i + 1] = arr[high];
    arr[high] = temp;

    return i + 1;
}

void quickSort(int arr[], int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

快速排序算法通过选择一个基准元素,将小于基准的元素放在左边,大于基准的元素放在右边,然后递归地对左右两部分进行排序。快速排序的平均时间复杂度为O(nlogn),效率相对较高。

从以上例子可以看出,算法的巧妙程度对解决问题方法的效率有很大影响。巧妙的算法能够通过优化计算步骤、减少不必要的操作等方式提高效率。因此,在实际开发中,我们应该选择和设计更巧妙的算法来解决问题,以提高方法的效率。

讨论1.3 再试一个多项式

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

抽象数据类型

抽象数据类型(Abstract Data Type,ADT)是指一个数学模型以及定义在此数学模型上的一组操作。它是一种用于描述数据类型的数学概念,而不涉及具体实现细节。ADT 将数据类型的逻辑特性和对这些特性进行操作的实际实现分离开来,使得用户可以只关注数据类型的逻辑特性而不用关心具体的实现方式。

举例来说,整数可以看作是一个抽象数据类型,我们知道整数具有加法、减法、乘法等操作,以及大小比较等逻辑特性,但我们无需关心整数的具体实现方式。在编程中,我们可以通过定义类或接口来表示抽象数据类型,并在其内部实现相应的操作,从而实现对该抽象数据类型的使用。

常见的抽象数据类型包括栈(stack)、队列(queue)、链表(linked list)、堆(heap)等。这些抽象数据类型都定义了一组操作,比如插入、删除、查找等,而具体的实现可以有多种方式,如数组、链表、树等。

总的来说,抽象数据类型提供了一种高层次的思考方式,使得程序员可以更加专注于数据类型的逻辑特性,而不必过多考虑具体的实现细节。
在这里插入图片描述
在这里插入图片描述
矩阵是一种常见的数学概念,在抽象数据类型中可以定义如下:

// 矩阵的抽象数据类型定义

template <typename T>
class Matrix {
public:
    // 构造函数,创建指定行列的矩阵
    Matrix(int rows, int cols);

    // 析构函数,释放矩阵内存
    ~Matrix();

    // 获取矩阵的行数
    int getRows() const;

    // 获取矩阵的列数
    int getCols() const;

    // 获取矩阵指定位置的元素
    T getElement(int row, int col) const;

    // 设置矩阵指定位置的元素
    void setElement(int row, int col, T value);

    // 矩阵加法
    Matrix<T> add(const Matrix<T>& matrix) const;

    // 矩阵减法
    Matrix<T> subtract(const Matrix<T>& matrix) const;

    // 矩阵乘法
    Matrix<T> multiply(const Matrix<T>& matrix) const;

    // 转置矩阵
    Matrix<T> transpose() const;

    // 打印矩阵
    void print() const;

private:
    int rows;  // 矩阵的行数
    int cols;  // 矩阵的列数
    T** data;  // 存储矩阵元素的二维数组指针
};

在上述定义中,我们使用了类模板来定义矩阵的抽象数据类型。通过模板参数 T,可以指定矩阵中元素的类型,比如整数、浮点数等。在类中定义了矩阵的基本操作,包括构造函数、析构函数、获取行列数、获取和设置矩阵元素、矩阵加减法、矩阵乘法、矩阵转置以及打印矩阵等操作。

这样的抽象数据类型定义使得用户可以方便地创建矩阵对象,并对矩阵进行各种操作,而不必关心具体的矩阵实现方式。同时,可以根据实际需要选择不同的矩阵实现方式,比如使用二维数组、使用向量容器等,从而灵活地满足不同场景下的需求。

抽象在软件开发中有什么好处

抽象在软件开发中具有许多好处,如下所示:

  1. 简化复杂性:抽象可以将复杂的问题和实体简化为高层次的概念和模型。通过隐藏底层的细节,用户只需关注于抽象的特性和行为,而不必了解其具体实现细节。这使得问题更易于理解和处理。

  2. 提高可维护性:通过抽象,可以将系统分解为模块化的组件,每个组件都有明确定义的接口。这样,当需要进行修改或扩展时,只需关注单个组件,而不会影响其他部分。这种解耦合的设计使得系统更加可维护和可扩展。

  3. 促进代码重用:抽象可以将通用的功能封装为独立的模块或类,并提供清晰的接口供其他模块使用。通过重用这些抽象的模块,可以避免重复编写相似的代码,提高代码的效率和可靠性。

  4. 提高代码可读性和可理解性:抽象可以提供更高级别的概念和命名,使代码更易于阅读和理解。通过使用适当的抽象概念和命名,可以使代码更贴近问题领域,并减少冗余的细节,使代码更易于阅读和维护。

  5. 降低耦合性:通过抽象,不同组件之间的依赖可以通过接口或抽象类来实现,而不是直接依赖于具体实现。这种松耦合的设计有助于减少模块之间的相互依赖,提高系统的灵活性和可扩展性。

总的来说,抽象是软件开发中一种重要的思想和技术,它可以帮助我们更好地理解和处理复杂的问题,并提供可维护、可扩展和高效的代码实现。通过合理使用抽象,可以提高开发效率、降低代码的复杂性,并提升软件质量和可读性。

算法

算法是一系列解决问题或完成任务的明确指令步骤。它是计算机科学和数学领域中的一个重要概念,用于描述解决特定问题的步骤和逻辑。

算法可以用来执行各种任务,如排序、搜索、数据处理、图形处理等。一个好的算法应该具有正确性(能够得出正确的结果)、可读性(易于理解和实现)、高效性(能够在合理的时间内完成任务)和鲁棒性(能够处理各种输入情况)等特点。

常见的算法包括冒泡排序、快速排序、二分查找、深度优先搜索、广度优先搜索等。算法的设计和分析是计算机科学的核心内容之一,它对于解决复杂的计算问题和优化计算过程非常重要。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

时间复杂度是衡量算法执行时间随输入规模增长而变化的度量。它表示算法执行所需的时间与问题规模之间的关系。

时间复杂度通常用大O符号(O)表示。下面是一些常见的时间复杂度:

  1. O(1):常数时间复杂度,表示算法的执行时间不随输入规模变化而变化,即执行时间是固定的。

  2. O(log n):对数时间复杂度,表示算法的执行时间随输入规模呈对数增长。

  3. O(n):线性时间复杂度,表示算法的执行时间与输入规模成线性关系。

  4. O(n log n):线性对数时间复杂度,表示算法的执行时间随着输入规模的增加而增加。

  5. O(n^2):平方时间复杂度,表示算法的执行时间随输入规模的平方增长。

  6. O(2^n):指数时间复杂度,表示算法的执行时间随输入规模呈指数增长。

从上述例子可以看出,时间复杂度越小,算法执行的效率越高。在实际应用中,我们希望选择时间复杂度较低的算法来解决问题,以提高程序的执行效率。但需要注意的是,时间复杂度只是一种理论上的分析方法,实际运行时间还受到算法实现的优化程度、硬件环境等因素的影响。

讨论 分析二分法

在这里插入图片描述
在这里插入图片描述

复杂度的渐进表示法

渐进表示法是用于描述算法时间复杂度的一种标记方法,常用的有大O符号(O)、Ω符号(Ω)和Θ符号(Θ)。它们分别表示最坏情况下、最好情况下和平均情况下的时间复杂度。

  1. 大O符号(O)表示算法的上界,用来描述最坏情况下的时间复杂度。O(g(n))表示在输入规模为n时,算法的执行时间不会超过g(n)的某个常数倍。例如,O(1)表示常数时间复杂度,O(n)表示线性时间复杂度,O(n^2)表示平方时间复杂度。

  2. Ω符号(Ω)表示算法的下界,用来描述最好情况下的时间复杂度。Ω(g(n))表示在输入规模为n时,算法的执行时间不会低于g(n)的某个常数倍。例如,Ω(1)表示常数时间复杂度,Ω(n)表示线性时间复杂度,Ω(n^2)表示平方时间复杂度。

  3. Θ符号(Θ)表示算法的紧确界,用来描述平均情况下的时间复杂度。Θ(g(n))表示在输入规模为n时,算法的执行时间在g(n)的某个常数倍范围内。例如,Θ(1)表示常数时间复杂度,Θ(n)表示线性时间复杂度,Θ(n^2)表示平方时间复杂度。

渐进表示法的优势在于它能够忽略算法执行中的常数因素和低阶项,从而更好地反映算法的增长趋势。例如,对于一个算法的时间复杂度为O(5n^2 + 2n + 1),根据渐进表示法,我们可以简化为O(n^2),因为当n足够大时,低阶项和常数因素对总体时间复杂度的影响较小。

需要注意的是,渐进表示法只描述了算法执行时间与输入规模之间的关系,而不考虑其他因素如硬件环境、编程语言等对算法执行时间的影响。因此,在实际应用中,对于具体问题的解决方案选择,还需综合考虑算法的时间复杂度以及其他方面的因素。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

应用示例:最大子列和问题

https://blog.csdn.net/liyuanyue2017/article/details/82990246

#include <iostream>

//KEY: use algorithm--Divide and Conquer;

using namespace std;
int *Init_array(int n);
int max(int a, int b, int c);
int divideseq(int *p, int left, int right);
int MaxSub(int *p, int n);

int main()
{
	int n, i = 0;
	cin >> n;
	int *p = Init_array(n);
	
	cout << MaxSub(p, n) << endl;
	delete[] p;

	return 0;
}

int* Init_array(int n)		//initialize array;
{
	int *pArray = new int[n];
	int i = 0;
	while (i < n)
		cin >> pArray[i++];

	return pArray;
}

int max(int a, int b, int c)
{
	return (a>b ? (a > c ? a : c) : (b < c ? c : b));
}

int divideseq(int *p, int left, int right)
{
	int i, maxboaderleft, maxboaderright;
	int leftboader = 0, rightboader = 0;

	if (left == right)	//it means only one data, the ending condition;
	{
		if (p[left] > 0)
			return p[left];
		else
			return 0;
	}

	int center = (left + right) / 2;
	int maxleft = divideseq(p, left, center);		// use 'recursion' to get the max left value;
	int maxright = divideseq(p, center + 1, right);

	maxboaderleft = 0;	
	for (i = center; i >= left; i--)		// get left boader max value, and right value, sum them.
	{
		leftboader += p[i];
		if (leftboader > maxboaderleft)		//so we get the value including the left and the right;
			maxboaderleft = leftboader;
	}

	maxboaderright = 0;
	for (i = center + 1; i <= right; i++)
	{
		rightboader += p[i];
		if (rightboader > maxboaderright)
			maxboaderright = rightboader;
	}

	return max(maxleft, maxright, maxboaderleft + maxboaderright);
}

int MaxSub(int *p, int n)
{
	return divideseq(p, 0, n - 1);
}

题目

在这里插入图片描述

给定K个整数组成的序列{ N1 _1
1

, N2 _2
2

, …, Nk _k
k

},“连续子列”被定义为{ N​i _{​i}
​i

​​ , N​i + 1 _{​i+1}
​i+1

​ , …, Nj _j
j

​​ },其中 1≤i≤j≤K。“最大子列和”则被定义为所有连续子列元素的和中最大者。例如给定序列{ -2, 11, -4, 13, -5, -2 },其连续子列{ 11, -4, 13 }有最大的和20。现要求你编写程序,计算给定整数序列的最大子列和。

本题旨在测试各种不同的算法在各种数据情况下的表现。各组测试数据特点如下:

数据1:与样例等价,测试基本正确性;
数据2:102 ^2
2
个随机整数;
数据3:103 ^3
3
个随机整数;
数据4:104 ^4
4
个随机整数;
数据5:105 ^5
5
个随机整数;
输入格式:
输入第1行给出正整数K (≤100000);第2行给出K个整数,其间以空格分隔。

输出格式:
在一行中输出最大子列和。如果序列中所有整数皆为负数,则输出0。

输入样例:

6
-2 11 -4 13 -5 -2

输出样例:

20
在这里插入图片描述
在这里插入图片描述

分析

我想到了三种方法,结合老师给的两种,总共五种方法
先给出 main 函数

int main(){
	int n;
	int a[100000+5];
	cin>>n;
	for(int i=0;i<n;i++)
		cin>>a[i];
	MaxSubseqSum1(n,a);
	MaxSubseqSum2(n,a);
	MaxSubseqSum3(n,a);
	MaxSubseqSum4(n,a);
	MaxSubseqSum5(n,a);
	return 0;
} 

算法一

最直接也是最直观的想法,一个循环控制子列的尾部,内嵌一个循环控制子列的头部,再内嵌一个循环来求解首部到尾部间子列和,每次求解完和更新最大值

/* 方法一:确定子列的首部和尾部,再遍历累加,时间复杂度 O(n^3)*/
int MaxSubseqSum1(int n,int a[]){
	int max = 0;
	for(int i=0;i<n;i++){   // 控制子列的尾部
		for(int k=0;k<i;k++){	  // 控制子列的头部
			int tmpSum = 0;   //临时存放头部到尾部子列和
			for(int j=k;j<=i;j++){   
				tmpSum+=a[j]; 
			}
			if(max < tmpSum)
				max = tmpSum;
		}
	}
	return max;
}
容易想到,复杂度也高,O ( n 3 ) O(n^3)O(n 3 )

算法二

考虑优化算法一,观察发现每次计算之后的子列和前面的子列都需要重新计算(比如计算 Sum(n+1)需要重新计算 Sum(n)),那我们可以这样优化,想办法能不能将每次计算的结果保存一下,即一个循环控制子列的首部,内嵌一个循环,既控制子列的尾部,也表示该段子列和,叠加一次更新一次最大值

/* 方法二:确定子列的首部,逐个累加,时间复杂度 O(n^2)*/ 
int MaxSubseqSum2(int n,int a[]){
	int max = 0;
	for(int i=0;i<n;i++){   // 控制子列的首部
		int tmpSum = 0;  // 当前子列和
		for(int j=i;j<n;j++){  // 控制子列的尾部
			tmpSum+=a[j];
			if(max < tmpSum)
				max = tmpSum;
		}
	}
	return max;
}
把之前控制尾部的循环和求解子列和的循环融合了,复杂度为 O ( n 2 ) O(n^2)O(n 2)

算法三

想法和算法二类似,不过算法二是控制首部,逐渐累加,算法三是控制尾部,逐渐减值。从首部出发可以自然的用一个数保存整段子列和,而从尾部出发则需要额外数组空间来保存子列和,额外数组空间首先保存其前 n 个数之和,然后每次减去当前值形成子列和

/* 方法三:确定子列的结尾,逐个减去子列前的数,时间复杂度 O(n^2)*/ 
int MaxSubseqSum3(int n,int a[]){
	int sum[100000+5];
	int max = 0;
	sum[0]=a[0];
	for(int i=1;i<n;i++)   // 预处理保存前 n 个数之和
		sum[i]=sum[i-1]+a[i];
	for(int i=0;i<n;i++){   // 控制尾部
		int tmpSum = sum[i];
		for(int j=0;j<=i;j++){   // 控制首部,每一次减去当前值即首尾子列和
			if(max < tmpSum)
				max = tmpSum;
			tmpSum-=a[j]; 
		}
	}
	return max;
}

把之前控制首部的循环和求解子列和的循环融合了,复杂度为 O ( n 2 ) O(n^2)O(n 2  )

算法四 分而治之

在这里插入图片描述

“分治法”,简单来说就是把一个大的问题分解成多个小问题求解,再从所有小的解里面寻求最优解。对于此问题而言,可以把一个大的序列分为两个小的序列,再把小的序列分为更小的两个序列,…,直到每个小序列只有一个数,这就是分的过程,在每个小序列中,会得到:

左边最大子列和(正数即本身,负数即0)
右边最大子列和
横跨划分边界的最大子列和
(比如对于只有 1 | 2 两个数的子列,其左边最大子列和为 1 ,右边最大子列和为 2,而横跨划分边界的最大子列和为 1+2)
此时三者中最大的值就是该小序列的"最大子列和",以此再得到更高层次的"最大子列和",…,最终得到整个问题的最大子列和

/* 方法四:递归分成两份,分别求每个分割后最大子列和,时间复杂度为 O(n*logn)*/
/* 返回三者中最大值*/
int Max3(int A,int B,int C){
	return (A>B)?((A>C)?A:C):((B>C)?B:C);
}
/* 分治*/
int DivideAndConquer(int a[],int left,int right){
	
	/*递归结束条件:子列只有一个数字*/
	// 当该数为正数时,最大子列和为其本身
	// 当该数为负数时,最大子列和为 0
	if(left == right){
		if(0 < a[left])  
			return a[left];
		return 0;
	}
	
	/* 分别递归找到左右最大子列和*/ 
	int center = (left+right)/2; 
	int MaxLeftSum = DivideAndConquer(a,left,center);
	int MaxRightSum = DivideAndConquer(a,center+1,right);
	
	/* 再分别找左右跨界最大子列和*/
	int MaxLeftBorderSum = 0;
	int LeftBorderSum = 0;
	for(int i=center;i>=left;i--){  //应该从边界出发向左边找
		LeftBorderSum += a[i];
		if(MaxLeftBorderSum < LeftBorderSum)
			MaxLeftBorderSum = LeftBorderSum;	
	}
	int MaXRightBorderSum = 0;
	int RightBorderSum = 0;
	for(int i=center+1;i<=right;i++){  // 从边界出发向右边找
		RightBorderSum += a[i];
		if(MaXRightBorderSum < RightBorderSum)
			MaXRightBorderSum = RightBorderSum;
	}
	
	/*最后返回分解的左边最大子列和,右边最大子列和,和跨界最大子列和三者中最大的数*/
	return Max3(MaxLeftSum,MaxRightSum,MaXRightBorderSum+MaxLeftBorderSum);
}
int MaxSubseqSum4(int n,int a[]){
	return DivideAndConquer(a,0,n-1);
}

时间复杂度 T ( n ) = 2 T ( T 2 ) + c ⋅ n , T ( 1 ) = O ( 1 ) T(n) = 2T(\frac {T}{2}) + c·n ,T(1) = O(1)T(n)=2T( 

T
​
 )+c⋅n,T(1)=O(1) ,即 T ( n ) = O ( n l o g n ) T(n) = O(nlogn)T(n)=O(nlogn)

算法五

“贪心法”,即不从整体最优上加以考虑,只做出某种意义上的局部最优解。其实最大子列和与它的首部和尾部都没有关系,我们只关心它当前的大小。当临时和加上当前值为负时,它对之后子列和肯定没有帮助(甚至只会让之后的和更小!),我们抛弃这段临时和将它置0

/* 方法五:直接累加,如果累加到当前的和为负数,置当前值或0,时间复杂度为 O(n)*/ 
int MaxSubseqSum5(int n,int a[]){
	int max = 0;
	int tmpSum=0;
	for(int i=0;i<n;i++){
		tmpSum+=a[i];
		if(tmpSum<0){
			tmpSum=0;
		}else if(max < tmpSum){
			max = tmpSum;
		}
	}
	return max;
}

显而易见的,时间复杂度为 O ( n ) O(n)O(n)

Maximum Subsequence Sum

题目
Given a sequence of K integers { N​1 _1
1

, N2 _2
2

​​ , …, NK _K
K

​​ }. A continuous subsequence is defined to be { N​i _i
i

​​ , N​i + 1 _{i+1}
i+1

​ , …, N​j _j
j

​​ } where 1 ≤ i ≤ j ≤ K 1≤i≤j≤K1≤i≤j≤K. The Maximum Subsequence is the continuous subsequence which has the largest sum of its elements. For example, given sequence { -2, 11, -4, 13, -5, -2 }, its maximum subsequence is { 11, -4, 13 } with the largest sum being 20.

Now you are supposed to find the largest sum, together with the first and the last numbers of the maximum subsequence.

Input Specification:
Each input file contains one test case. Each case occupies two lines. The first line contains a positive integer K (≤10000). The second line contains K numbers, separated by a space.

Output Specification:
For each test case, output in one line the largest sum, together with the first and the last numbers of the maximum subsequence. The numbers must be separated by one space, but there must be no extra space at the end of a line. In case that the maximum subsequence is not unique, output the one with the smallest indices i and j (as shown by the sample case). If all the K numbers are negative, then its maximum sum is defined to be 0, and you are supposed to output the first and the last numbers of the whole sequence.

Sample Input:

10
-10 1 2 3 4 -5 -23 3 7 -21

Sample Output:

10 1 4

分析

最大子列和问题加难版
翻译过来就是求一个序列的最大子列和,并输出最大子列和的开头和结尾的数,求最大子列和我们用到了"贪心法",累加当前序列,如果序列和小于 0 就置 0 重新累加,否则更新最大值。
现在的问题是如何记录开头和结尾的数,结尾的数,就是每次更新最大值时记录当时的序列号,该序列号存的值就是结尾的数;而开头的数和每次序列和小于 0 相关,每次序列和清零表示抛弃了这段子序列,之后有新的开始,所以清 0 后下一个序列号里存的就是子序列开头的数,那么如何记录下这个数?对了,更新最大值的时候!每次更新最大值相当于固定一段子序列
最后一个问题就是,当只有一个 0,其余全为负数的情况,按道理应该输出全 0,但是实际输出的是 “0 第一个数 最后一个数”,为了避免这种情况,我们将最大值初值赋为 -1,这样遇到 0 时也能更新最大值

#include<iostream>
using namespace std;
int main(){
	int n;
	int a[100000+5];
	cin>>n;
	for(int i=0;i<n;i++)
		cin>>a[i];
	int left = 0;
	int tmpleft = 0;
	int right = n-1;
	int max =0;
	int tmpSum=0;
	for(int i=0;i<n;i++){
		tmpSum+=a[i];
		if(tmpSum<0){
			tmpSum=0;
			tmpleft = i+1;   // 开头的数就在这段被抛弃子序列的下一个 
		}else if(max < tmpSum){
			max = tmpSum;        
			left = tmpleft;   // 每次更新最大值也就确定了一段子序列,保存此时的开头 
			right = i;       // 结尾的数就在每次更新最大值时 
		}
	}
	if(max>=0)
		cout<<max<<" "<<a[left]<<" "<<a[right];
	else
		cout<<0<<" "<<a[0]<<" "<<a[n-1];
	return 0;
} 

二分查找

分析

这题就是写一个二分查找算法的函数接口
基本思路没问题,将要查找的值和当前范围内的中值比较,有三种情况:

要查找的目标值 < 中值,舍弃中值右半部分,缩减范围
要查找的目标值 > 中值,舍弃中值左半部分,缩减范围
要查找的目标值 = 中值,不就找到了吗,直接返回结果
这个查找是一个不确定循环次数的循环,所以用 while,跳出循环的条件是:

找到了,即上面第三种情况,直接 return 即可
找完了,并没有!当界定左范围的值大于界定右范围的值(考虑一下如果相等呢?),表示已经找完,此时返回 NotFound

代码(cpp)
Position BinarySearch( List L, ElementType X ){
	Position left = 1;
	Position right = L->Last;
	while(left<=right){  // 考虑一下这为什么要取等?
		Position center = (left+right)/2;  //先找中间值 
		if(L->Data[center] < X){     //比中间值大,X 在右半边 
			left = center+1;
		}else if(X < L->Data[center]){   //比中间值小,X 在左半边 
			right = center-1;
		}else  //找到了,直接返回 
			return center;
	}
	return NotFound;
} 

每次缩减一半范围,所以其复杂度为 O ( l g n ) O(lgn)O(lgn)

二分查找是一种在有序数组中查找特定元素的高效算法。这种搜索算法的工作原理是先确定数组的中间元素,然后将待查找的值与中间元素进行比较。如果待查找的值等于中间元素,则找到了匹配的项;如果待查找的值小于中间元素,则在数组的前半部分继续查找;如果待查找的值大于中间元素,则在数组的后半部分继续查找。通过这种方式,每次比较都能使搜索范围减半,从而实现了较快的搜索速度。

下面是一个用C语言实现的简单的二分查找算法示例:

#include <stdio.h>

int binarySearch(int arr[], int l, int r, int x) {
    while (l <= r) {
        int mid = l + (r - l) / 2;

        // 如果找到元素,则返回元素的索引
        if (arr[mid] == x)
            return mid;

        // 如果待查找的值小于中间元素,则在左半部分继续查找
        if (arr[mid] > x)
            r = mid - 1;
        // 如果待查找的值大于中间元素,则在右半部分继续查找
        else
            l = mid + 1;
    }

    // 如果未找到元素,则返回-1
    return -1;
}

int main() {
    int arr[] = {2, 3, 4, 10, 40};
    int n = sizeof(arr) / sizeof(arr[0]);
    int x = 10;
    int result = binarySearch(arr, 0, n - 1, x);
    if (result == -1)
        printf("元素不在数组中");
    else
        printf("元素在数组中的索引为 %d", result);
    return 0;
}

这段代码演示了如何使用二分查找算法在有序数组中查找特定元素。在main函数中,首先定义了一个有序数组arr和待查找的值x,然后调用binarySearch函数来查找x在数组中的位置。最终根据函数的返回值输出相应的结果。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

神经网络常见评价指标AUROC(AUC-ROC)、AUPR(AUC-PR)

神经网络的性能可以通过多个评价指标进行衡量&#xff0c;具体选择哪些指标取决于任务的性质。以下是神经网络中常见的评价指标&#xff1a; 准确性&#xff08;Accuracy&#xff09;&#xff1a; 准确性是最常见的分类任务评价指标&#xff0c;表示模型正确预测的样本数占总样…

【小黑送书—第六期】>>AI时代,程序员如何应对挑战——《AI时代系列书籍》

在AI时代&#xff0c;程序员面临着新的机遇和挑战。为了适应这个快速发展的时代&#xff0c;掌握新技能并采取相应的应对策略是至关重要的。 对于办公人员或程序员来说&#xff0c;利用AI可以提高工作效率。例如&#xff0c;使用AI助手可以帮助自动化日常的重复性工作&#xff…

【Mycat2实战】二、Mycat安装部署

1. Mycat下载 Mycat官网下载地址&#xff0c;点击直接前往&#xff1a;http://www.mycat.org.cn/ Mycat 有提供编译好的安装包&#xff0c;支持 windows、Linux、Mac、 Solaris 等系统上安装与运行。 本文及后续系列的文章都是使用Linux的系统进行操作。 这里我们选择使用文…

【保姆级教程】Linux安装JDK8

本文以centos7为例&#xff0c;一步一步进行jdk1.8的安装。 1. 下载安装 官网下载链接&#xff1a; https://www.oracle.com/cn/java/technologies/downloads/#java8 上传jdk的压缩包到服务器的/usr/local目录下 在当前目录解压jdk压缩包&#xff0c;如果是其它版本&#xf…

Java --- JVM之StringTable

目录 一、String的基本特性 二、String的内存分配 2.1、String内存分布图 三、字符串拼接操作 3.1、字符串拼接操作底层原理 3.2、拼接操作与append操作效率对比 四、intern()方法 4.1、intern()效率 五、StringTable的垃圾回收 一、String的基本特性 1、String字符…

彩票双色球预测工具1.0

搏一搏 单车变跑车 祝各位开出大奖&#xff01;&#xff01; 后续会持续更新&#xff0c;欢迎关注&#x1f44f; from random import randintdef gener_blue_ball():return randint(1, 33)def gener_blue_ball_s():blue_ball_set set()while True:if len(blue_ball_set) 6:b…

【数据结构】希尔排序(最小增量排序)

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前正在学习c和算法 ✈️专栏&#xff1a;数据结构 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章有啥瑕疵 希望大佬指点一二 如果文章对你有帮助…

【数据结构初阶】链表OJ

链表OJ 题目一&#xff1a;移除链表元素题目二&#xff1a;反转链表题目三&#xff1a;链表的中间节点题目四&#xff1a;链表中倒数第k个结点题目五&#xff1a;合并两个有序链表题目六&#xff1a;链表分割题目七&#xff1a;链表的回文结构题目八&#xff1a;相交链表题目九…

JavaScript概述

一、JavaScript简介&#xff1a; JavaScript是互联网上流行的脚本语言&#xff0c;可用于HTML和web&#xff0c;可广泛应用于服务器、PC、笔记本、平板电脑和智能手机等设备。 JavaScript是一种轻量级的编程语言&#xff0c;可插入HTML页面的编程代码&#xff0c;插入HTML页面后…

联盟 | Quick Creator X HelpLook ,AI助力打造出海企业内容营销

随着人工智能的快速发展&#xff0c;各行各业也加快了人工智能工具的探索&#xff0c;积极将AI融入到行业发展中&#xff0c;出海外贸也不例外。AI渐渐地融入到外贸的各个环节&#xff0c;为企业出海提速增效。 为此&#xff0c;「 Quick Creator 」与 「 HelpLook 」达成战略合…

mask-rcnn原理与实战

一、Mask R-CNN是什么&#xff0c;可以做哪些任务&#xff1f; Mask R-CNN是一个实例分割&#xff08;Instance segmentation&#xff09;算法&#xff0c;可以用来做“目标检测”、“目标实例分割”、“目标关键点检测”。 1. 实例分割&#xff08;Instance segmentation&am…

Python爬虫的七个常用技巧总结,这些你一定得知道!

文章目录 前言1、基本抓取网页2、使用代理IP3、Cookies处理4、伪装成浏览器5、验证码的处理6、gzip压缩7、多线程并发抓取关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战…

前端element的el-tooltip鼠标经过显示文字,没有文字显示空黑框问题

场景&#xff1a; 有时候在使用element的el-tooltip时会使用三元表达式&#xff0c;满足某个条件后才显示提示文字&#xff0c;否则不展示文字&#xff0c;但是却出现在在没有文字时展示了黑框&#xff0c;如下图&#xff1a; 解决方案&#xff1a; 加一个disabled便可&#…

手机维修店服务预约小程序的效果怎样

手机维修店在每个城市的商业街或商场里都很常见&#xff0c;目前人手一个或多个手机&#xff0c;虽然手机质量有了提升&#xff0c;但各种毛病依然无法避免&#xff0c;因此手机维修店虽然不是人挤人&#xff0c;但整体生意还可以&#xff0c;各种服务也愈加完善。 而市场接受…

人力物力和时间资源有限?守住1个原则,精准覆盖所有兼容性测试!

随着 APP 应用范围越来越广&#xff0c;用户群体越来越大&#xff0c;终端设备的型号也越来越多&#xff0c;移动终端碎片化加剧&#xff0c;使得 APP 兼容性测试成为测试质量保障必须要考虑的环节。 APP 兼容性测试通常会考虑&#xff1a;操作系统、厂家 ROM、屏幕分辨率、网…

掌握Python中的控制流语句:break, continue, quit的应用技巧详解

引言 在Python编程中&#xff0c;控制流语句是非常重要的一部分&#xff0c;它们可以帮助我们控制程序的执行流程。其中&#xff0c;break、continue和quit是常用的控制流语句&#xff0c;它们可以在循环中起到关键作用。本文将详细介绍这些控制流语句的应用技巧&#xff0c;帮…

7天入门python系列之爬取热门小说项目实战,互联网的东西怎么算白嫖呢

第七天 Python项目实操 编者打算开一个python 初学主题的系列文章&#xff0c;用于指导想要学习python的同学。关于文章有任何疑问都可以私信作者。对于初学者想在7天内入门Python&#xff0c;这是一个紧凑的学习计划。但并不是不可完成的。 学到第7天说明你已经对python有了一…

Playwright UI 自动化测试实战

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

VSCode配置MingW编译调试环境

1.MingW简介 MinGW&#xff0c;即 Minimalist GNU For Windows。它是一些头文件和端口库的集合&#xff0c;该集合允许人们在没有第三方动态链接库的情况下使用 GCC&#xff08;GNU Compiler C&#xff09;产生 Windows32 程序。 实际上 MinGW 并不是一个 C/C 编译器&#xf…