《算法笔记》阅读笔记

这是之前刷PAT时看胡凡的《算法笔记》时做的一点笔记,后来没时间,就没看完。

emmm,刚开始看的内容都很基础。现在想想这样从头到尾地学确实没必要,可能当时的目标也和现在不一样吧。

第一章 如何使用本书

在线评测系统

PAT是“单点测试”。

常见的评测结果

==整理常见的测评结果==

第二章 C/C++快速入门

cincout消耗的时间比scanfprintf多得多,很多题目可能输入还没结束就超时了。……。请不要同时在一个程序中使用coutprintf,有时候会出问题。

头文件的hhead的缩写

C++向下兼容C

==整理一下向下兼容、向上兼容、向前、向后==

基本数据类型

int\(-2^{31}\sim+(2^{31}-1)\),大致范围是\(-2\times10^9\sim2\times10^9\),绝对值在\(10^9\)以内的整数都可以定义成int型。

long long\(-2^{63}\sim+(2^{63}-1)\),大致范围是\(-9\times10^{18}\sim9\times10^{18}\)

浮点型都用double来存储。

小写字母比大写字母的ASCII码大32。

\0代表空字符NULL,其ASCII码为0,请注意\0不是空格。

整型常量在赋值给布尔型变量时会自动转换为true(非零)和false(零)。

宏定义是直接将对应的部分替换,然后才进行编译和运行。

输入和输出

类似于13:45:20这种hh:mm:ss的时间需要输入,可以使用下边的代码:

int hh,mm,ss;
scanf("%d:%d:%d",&hh,&mm,&ss);

scanf的双引号内的内容其实就是整个输入,只不过把数据换成它们对应的格式符并把变量的地址按次序写在后面而已。

除了%c以外(scanf%c格式是可以读入空格和换行的),scanf对其他格式符(如%d)的输入是以空白符(即空格、Tab)为结束判断标志的,字符数组使用%s读入的时候以空格和换行为读入结束的标志。

double输出格式为%f,输入格式为%lf

%.mf保留\(m\)位小数,这个“保留”使用的是精度的“四舍六入五成双”规则。

getchar可以识别换行符。

常用数学函数

round函数,四舍五入。

==整理常用数学函数==

条件判断

条件判断中n!=0可以改为nn==0可以改为!n

循环语句

do...while会先执行循环体一次,然后才去判断循环条件是否为真,这就使得do...while语句的实用性远不如while,因为用户碰到的大部分情况都需要能处理在某些数据下不允许进入循环的情况。

C语言中不允许随时定义临时变量,C++可以。

一维数组

数组大小必须是整数常量,不可以是变量。

如果只初始化了一维数组的一部分元素,后面未被初始化的元素将会由不同编译器内部实现的不同而被赋以不同的初值,而一般情况默认初值为0。所以可以通过下面的代码实现整个数组赋初值0

int a[10]={};
int a[10]={0};

递推可以分为顺推和逆推两种。

冒泡排序的本质在于交换,即每次通过交换的方式把当前剩余元素的最大值移动到一端,当剩余元素数量减少为0时,排序结束。代码如下:

// 定义含5个元素的数组
int a[]={3,1,4,5,2};
int n=5;

// 冒泡排序(从小到大)
for(int i=1;ia[j+1]){    // 交换
            int temp=a[j];
            a[j]=a[j+1];
            a[j+1]=temp;
        }
    }
}

// 输出数组元素
for(int i=0;i

二维数组

可以把二维数组当作一维数组的每一个元素都是一个一维数组。

二维数组初始化(下面的代码中未被初始化的元素一般默认为0):

int a[5][6]={{3,1,2},{8,4},{},{1,2,3,4,5}};

如果数组大小较大(大概\(10^6\)级别),则需要定义在主函数外边。原因是函数内部申请的局部变量来自系统栈,允许申请的空间较小。而函数外部申请的全局变量来自静态存储区,允许申请的空间较大。

memset

一般用函数memsetfill给数组中每一个元素赋相同的值。

使用memset需要使用头文件string.h,使用fill需要使用STL中的头文件algorithm

memset格式如下:

memset(数组名,值,sizeof(数组名));

只建议初学者使用memset赋0和-1,因为memset是按字节赋值,即对每个字节赋同样的值,0的二进制补码为全0,-1的二进制补码为全1,不容易弄错。如果要对数组赋其它值,请使用fill函数(但memset的执行速度快)。

字符数组

可以使用字符串对字符数组进行初始化,但也仅限于初始化。

%c能够识别空格和换行并将其输入,%s`通过空格或换行来识别一个字符串的结束。

gets识别换行符作为输入结束(并会将其读取走),因此scanf完一个整数后,如果要用gets,需要用getchar接受整数后的换行符。

一维字符数组的末尾都会有一个\0,表示字符串的结束。使用getsscanf时会自动在字符串后边添加\0putsprintf也通过识别\0作为字符串的结尾来输出字符串。

string.h

  • strlen(字符数组1)

    得到字符数组\0前边的字符数

  • strcmp(字符数组1,字符数组2)

    按照字典序比较两个字符串的大小,返回一个整数(负数,0,正数),符号和字符串1-字符串2相同。

  • strcpy(字符数组1,字符数组2)

    把字符串2(第二个参数)赋值给字符串1(第一个参数)

  • strcat(字符数组1,字符数组2)

    把字符数组2接到字符数组1后边

  • sscanf(字符数组,"%d",&n)

    从字符数组中读

  • sprintf(字符数组,"%d",n)

    往字符数组中写

数组作为函数参数

数组作为参数时,参数中数组的第一维不需要填写长度(如果是二维数组,那么第二维需要填写长度),实际调用时

指针

指针是一个unsigned类型的整数

int* p1,p2; //p1是int型指针 p2是int型变量
int* p1,*p2;    //p1和p2都是int型指针

cin

char str1[100];
cin.getline(str1,100);

string str2;
getline(cin,str2);

浮点数的比较

由于计算机采用有限位的二进制编码,因此浮点数在计算机的存储并不总是精确的,于是需要引入一个极小的EPS来对这种误差进行修正。

==待补充:通过EPS进行浮点数的比较==

复杂度

  • 时间复杂度

    在时间复杂度中,高等级的幂次会覆盖低等级的幂次。当有些算法实现较为复杂时,其常数会比较大,这时即便时间复杂度(一般讲时间复杂度是不带系数的)相同,其性能也会有较大差距。

    对一般的OJ系统来说,1秒能承受的运算次数大概是\(10^7\sim10^8\),因此\(O(n^2)\)的算法当n的规模是1000时是可以承受的,而当n的规模为100000时则是不可承受的。

  • 空间复杂度

    在一般的应用中,一般来说空间都是足够使用的(只要不开好几个\(10^7\)以上的数组即可,例如int A[10000][10000]的定义就是不合适的)。

    \(O(1)\)的空间复杂度指的是算法消耗的空间不随数据规模的增大而增大。

    考虑到空间一般够用,因此常常采用以空间换时间的策略。

  • 编码复杂度

    编码复杂度是一个定性的概念,并没有什么量化的标准。

黑盒测试

黑盒测试是指:系统后台会准备若干组输入数据,然后将其输到提交的程序中,如果输出的结果与正确答案完全相同(字符串意义上的比较),那么就称通过了这道题的黑盒测试,否则会根据错误类型而返回不同的结果。

根据黑盒测试是否对每组输入数据都单独测试或是一次性测试所有测试数据,又可以分为单点测试多点测试

==整理多点测试的几种类型==

第三章 入门篇(1)——入门模拟

第四章 入门篇(2)——算法初步

简单选择排序

选择排序是最简单的排序算法之一,本节介绍众多选择排序方法中最常用的简单选择排序

将数组分为前后两部分:有序部分和无序部分。遍历数组,每次选择最小的值放在数组前边(有序部分),时间复杂度为\(O(n^2)\)

// 定义含5个元素的数组
int a[]={3,1,4,5,2};
int n=5;

// 选择排序(从小到大)
for(int i=0;i

插入排序

插入排序也是最简单的一类排序方法,本节主要介绍众多插入排序方法中最直观的直接插入排序

把序列分为左右两部分:有序(左)和无序(右),从无序的部分取出元素,插入有序序列对应位置。

// 定义含5个元素的数组
int a[]={3,1,4,5,2};
int n=5;

// 直接插入排序(从小到大)
for(int i=1;i0&&temp>a[j-1]){
        a[j]=a[j-1];
        j--;
    }
    
    // 插入
    a[j]=temp;
}

// 输出数组元素
for(int i=0;i

散列的定义和整数散列

散列(hash)是常见的算法思想之一,在很多程序中都会有意无意地使用到。

若给出\(N\)个正整数,再给出\(M\)个正整数,问这M个数中的每个数分别是在\(N\)个数出现过,其中\(N,M\leq10^5\),且所有正整数均不超过\(10^5\)

对这个问题,最直接的思路是:对每个欲查询的正整数\(x\),遍历\(N\)个数,看是否有一个数与x相等。这种做法的时间复杂度是\(O(NM)\),当\(N\)\(M\)都很大(\(10^5\)级别)时,显然是无法承受的。

不妨用空间换时间,即设定一个bool型数组hashTable[100010],其中hashTable[x]==true表示正整数\(x\)\(n\)个正整数中出现过。这样就可以在一开始读入\(N\)个正整数时就对hashTable进行赋值,于是对于\(M\)个欲查询的数,就能直接通过hashTable判断出每个数是否出现过。显然这种做法的时间复杂度为\(O(M+N)\)

同样的,如果题目要求统计次数而非是否出现,就把数组改成int型,这两个问题的解法都有一个特点,那就是直接把输入的数作为数组的下标来对这个数的性质进行统计(这种做法非常实用,请务必掌握)。这是一个很好的用空间换时间的策略,因为它将查询的复杂度降到了\(O(1)\)级别。

但这个策略暂时还有一个问题——上面的题目中出现的每个数都不会超过\(10^5\),因此直接作为数组下标是可行的,但是如果输入可能是\(10^9\)大小的整数,或者甚至是一个字符串,就不能将它们直接作为数组下标了。

这时可以使用散列。一般来说,散列可以浓缩成“将元素通过一个函数转换为整数,使得改正数可以尽量唯一地代表这个元素”,其中把这个转换函数称为散列函数H,也就是说,如果元素在转换前为key,那么转换后就是一个整数H(key)

对于key是整数的情况来说,常用的散列函数有直接定址法、平方取中法、除留余数法。

H(key1)==H(key2),这种情况叫作冲突常用的解决冲突的方法有线性探查法、平方探查法和链地址法,其中前两种都计算了新的hash值,又称为开放定址法。

在写代码时,这种散列的功能可以用STL中的map代替。

字符串hash初步

一个点\(P\)的坐标\((x,y)\)可以用下面的散列函数进行处理:

\(H(P)=x\times Range+y\),这样对数据范围内的任意两个整点\(P_1\)\(P_2\)\(H(P_1)\)都不会等于\(H(P_2)\)

字符串hash是指将一个字符串\(S\)转换为一个整数,使得该整数可以尽可能地唯一地代表字符串\(S\)。本节只讨论将字符串转换为唯一的整数。

假设字符串\(S\)由大写字母\(A \sim Z\)构成。在这个假设下,可以把26个大写字母视为\(0\sim25\),进而转换为二十六进制,再转换为十进制,可得一唯一整数。只是\(S\)的长度并不可太长。

在上面的假设下,假如还可以由\(a\sim z\)组成,可以再把\(a\sim z\)对应为\(26\sim51\),进而转换为五十二进制,再转换为十进制。

分治

分治法(divide and conquer)将原问题划分成若干个规模较小而结构与原问题相同或相似的子问题,然后分别地解决这些子问题,最后合并子问题的解,即可得到原问题的解。

分治法的三个步骤:

  1. 分解

    将原问题分解为若干和原问题拥有相同或相似结构的子问题

  2. 解决

    递归求解所有子问题。如果存在子问题的规模小到可以直接解决,就直接解决它。

  3. 合并

    将子问题的解合并成原问题的解。

分治法分解出的子问题应当是相互独立、没有交叉的。如果存在两个子问题有相交部分,那么不应当使用分治法解决。

广义上来讲,分治法分解成的子问题个数只要大于0即可。从严格的定义上讲,一般把子问题的个数为1的情况称为减治,而把子问题个数大于1的情况称为分治。

分治法作为一种算法思想,既可以使用递归的手段去实现,也可以通过非递归的手段去实现。

递归

递归有两个重要概念:

  1. 递归边界
  2. 递归式

\(n\)的阶乘体现了减治的思想,求\(Fibonacci\)数列的第\(n\)项体现了分治的思想。

一般把\(1\sim n\)\(n\)个整数按某个顺序摆放的结果称为这\(n\)个整数的一个排列,而全排列指这\(n\)个整数能形成的所有排列。

\(n\)皇后问题就可以使用解决全排列问题的方法去解决。

如果在到达递归边界前的某层,由于一些事实导致已经不需要往任何一个子问题递归,就可以直接返回上一层。一般把这种做法称为回溯法

简单贪心

贪心法是求解一类最优化问题的方法,它总是考虑当前状态下局部最优(或较优)的策略,来使全局的结果达到最优(或较优)。

可以使用贪心法的问题一定满足最优子结构性质,即一个问题的最优解可以由它的子问题的最优解构造出来。

要获得全局最优解,则要求中间的每步策略都是最优的,因此严谨使用贪心法来求解最优哈问题需要对采取的策略进行证明。证明的一般思路是使用反证法及数学归纳法,即假设策略不能导致最优解,然后通过一系列推导来得到矛盾,以此证明策略是最优的,最后用数学归纳法保证全局最优。不过对于平常来说,不太容易对想到的策略进行严谨的证明(贪心的证明往往比贪心本身更难),如果在想到某个似乎可行的策略,并且自己无法举出反例,那就勇敢地实现它。

区间贪心

区间不相交问题和区间选点

二分查找

二分查找是基于有序序列的查找算法,二分查找的高效之处在于,每一步都可以去除当前区间中的一半元素,因此其时间复杂度是\(O(logn)\)

如果序列是严格递增

  • 递归方法
#include 
using namespace std;

int binarySearch(int* arr,int left,int right,int key);

int main()
{
    // 定义含10个元素的升序数组
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int n = 10;
    
    // 查询key
    int key=15;
    printf("%d\n", binarySearch(a, 0, n-1, key));

    system("pause");
    return 0;
}

// 在数组arr的[left,right]中寻找key,找到则返回key在数组中的位置,否则返回-1
// 初始区间一般是[0,n-1]
int binarySearch(int *arr, int left, int right, int key)
{
    // 区间正确
    if(left<=right){
        // 设置区间中间下标
        int mid = (left + right) / 2;

        // 找到
        if (key == arr[mid])
        {
            return mid;
        }
        // 左半区间查找
        else if (key < arr[mid])
        {
            return binarySearch(arr, left, mid - 1, key);
        }
        // 右半区间查找
        else
        {
            return binarySearch(arr, mid+1,right, key);
        }
    }

    // 区间错误
    return -1;
}
  • 非递归方法

在程序设计时,更多采用的是非递归的写法。

#include 
using namespace std;

int binarySearch(int* arr,int left,int right,int key);

int main()
{
    // 定义含10个元素的升序数组
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int n = 10;
    
    // 查询key
    int key=19;
    printf("%d\n", binarySearch(a, 0, n-1, key));

    system("pause");
    return 0;
}

// 在数组arr的[left,right]中寻找key,找到则返回key在数组中的位置,否则返回-1
// 初始区间一般是[0,n-1]
int binarySearch(int *arr, int left, int right, int key)
{
    int mid;

    //当可以形成区间时,进行查找
    while(left<=right){
        // 设置区间中间下标
        mid = (left + right) / 2;

        // 找到,返回对应位置
        if (key==arr[mid]){
            return mid; 
        }
        // key在左半区间,更新right
        else if(key

如果序列是严格递减

只需要把if(key语句中的<改成>就好了。

如果序列是非严格递增(即递增,但元素可能重复)

如何求出序列中第一个大于等于key的元素的位置L和第一个大于key的元素的位置R,这样元素key在序列中的存在区间就是左闭右开区间\([L,R)\)。显然,如果序列中没有key,那么LR可以理解为假设序列中存在keykey应该在的位置。

上面有两个问题:

  • L
#include 
using namespace std;

int lowerBound(int *arr, int left, int right, int key);

int main()
{
    // 定义含10个元素的升序数组
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int n = 10;
    
    // 查询key
    int key=4;
    printf("%d\n", lowerBound(a, 0, n, key));

    system("pause");
    return 0;
}

// 在非严格递增数组arr的[left,right]中寻找第一个大于等于key的值的位置,如果不存在则返回该值应该在的位置(即最后一个元素后边)
// 初始区间一般是[0,n]
int lowerBound(int *arr, int left, int right, int key)
{
    int mid;

    // 当left==right时,刚好求出大于等于key的第一个数字的位置
    while(left
  • R
#include 
using namespace std;

int upperBound(int *arr, int left, int right, int key);

int main()
{
    // 定义含10个元素的升序数组
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int n = 10;
    
    // 查询key
    int key=4;
    printf("%d\n", upperBound(a, 0, n, key));

    system("pause");
    return 0;
}

// 在非严格递增数组arr的[left,right]中寻找第一个大于key的值的位置,如果不存在则返回该值应该在的位置(即最后一个元素后边)
// 初始区间一般是[0,n]
int upperBound(int *arr, int left, int right, int key)
{
    int mid;

    // 当left==right时,刚好求出大于等于key的第一个数字的位置
    while(left

对比lowerBoundupperBound的代码可知,upperBound函数只是把代码中的if(key<=arr[mid])改成if(key,其他完全相同,这启发我们去寻找它们的共同点。

可以发现,两者都在解决这样一个问题:寻找有序序列中第一个满足某条件C的元素的位置。这是一个非常重要且经典的问题,平常能碰到的大部分二分法问题都可以归结于这个问题。显然,所谓的条件C在序列中一定是从左到右先不满足,然后满足的。该类问题代码模板如下:

int solve(int left,int right)
{
    int mid;
    while(left

若想寻找最后一个满足条件C的元素的位置,则可以先求第一个满足条件!c的元素的位置,然后将该位置减1即可。

如果目的是判断有序序列中是否存在满足某条件的元素,使用刚开始的二分最合适。

二分法的其他应用

上面讲了二分查找,事实上二分法的应用远不止如此。

估算\(\sqrt{2}\)

#include 
using namespace std;

int main()
{
    // 误差
    const double eps=10e-5;

    // 
    double left=1,right=2;
    double mid;

    // 逼近2^0.5
    while(right-left>eps){
        // 计算mid
        mid = left+(right - left) / 2;

        if (2>mid * mid){
            left=mid;            
        }
        else if(2

装水问题

有一个侧面看上去是半圆的储水装置,该半圆的半径是\(R\),要求往里面装入高度为\(h\)的水,使其在侧面看去的面积与半圆面积的比例恰好为\(r\)。已知\(r\),求\(h\)

#include 
#include 
using namespace std;

int main()
{
    // 一些常量
    const double eps=10e-5;
    const double r=0.5;
    const double R=1;
    const double pi=3.1415926535;

    // 
    double left=0,right=R;
    double mid,alpha,L,S1,S2=pi*R*R/2;

    // 逼近
    while(right-left>eps){
        // 计算mid
        mid = left+(right - left) / 2;
        alpha=2*acos((R-mid)/R);
        L = 2 * sqrt(R * R - (R - mid) * (R - mid));
        S1 = alpha*R*R/2-L*(R-mid)/2;

        if (r>S1/S2){
            left=mid;            
        }
        else if (r < S1 / S2){
            right=mid;
        }
    }
    
    // 输出结果
    mid = left + (right - left) / 2;
    printf("%f\n",mid);

    system("pause");
    return 0;
}

木棒切割

给出\(N\)根木棒,长度均已知,现在希望通过切割它们来得到至少\(K\)段长度相等的木棒(长度必须是整数),问这些长度相等的木棒最长能有多长。例如对于三根长度分别为10、24、15的木棒来说,假设\(K=7\),即至少需要7段长度相等的木棒,那么可以得到的最大长度为6(\((10+24+15)/7=7\)),在这种情况下,第一根木棒可以提供\(10/6=1\)段、第二根木棒可以提供\(24/6=4\)根、第三根木棒可以提供\(15/6=2\)根,达到了7根的要求。

对于这道题,我们可以注意到一个结论:如果长度相等的木棒的长度\(L\)越长,那么热可以得到的木棒段数\(k\)越小。

==P142==


作者:@臭咸鱼

转载请注明出处:https://www.cnblogs.com/chouxianyu/

欢迎讨论和交流!


你可能感兴趣的:(《算法笔记》阅读笔记)