数据结构学习笔记

一、小技巧

1、计算多项式

//  实现f(x)=a0+a1*x+a2*x^2+...+an*x^n
//  可化简为f(x)=a0+x*(a1+x*(...(an-1+x*an)...))
double f(double *a,int n,double x)
{
    double p = a[n];
    for (int i=n;i>0;i--) {
        p = a[i-1] + x*p;
    }
    return p;
}

2、计算最大连续子序列和

//  实现f(i,j)=max{0,∑(k=i,j)a[k]}
int a[];
int MaxSubseqSum(int n)
{
    int MaxSum,ThisSum;
    MaxSum = ThisSum = 0;
    for (int i=0;i<n;i++) {
        ThisSum += a[i];
        /*如果当前序列之和大于最大序列和就更新*/
        if ( ThisSum > MaxSum ) {
            MaxSum = ThisSum;
        }
  /*如果序列之和是负数,说明以后的元素被加上后也无法达到最大*/
        else if ( ThisSum < 0 ) {
            ThisSum = 0;	//更新当前序列之和
        }
    }
    return MaxSum;
}

二、线性表

1、十字链表的使用思路
矩阵可以使用二维数组进行存储,但是有以下缺陷:
<1> 必须事先知道矩阵的大小
<2> 若矩阵是稀疏的,则会造成巨大的空间浪费

因此,我们可以使用十字链表来只存储非0项
用十字链表建立结点:数据域分为 行Row、列Col、值Value,指针域分为 行指针Right、列指针Down

其中第一个结点表示行数和列数以及非0项的个数,此后每行每列都构成一个循环链表
在这里插入图片描述
由于Term和Head具有共性,所以可以用Union把他们合并起来在这里插入图片描述
2、特殊的线性表——堆栈

<1> 堆栈的顺序存储

若用一个数组模拟两个堆栈,要求最大限度利用数组,有空间就能入栈。我们可以将数组对半分开,以中间为切入点,则下标为-1或Maxsize的地方就为栈底。

若两个堆栈为空,则Top1=-1,Top2=Maxsize
若两个堆栈均满,说明两个指向顶端的标记挨在一起,所以Top2-Top1=1


若用链表来模拟堆栈,则应该以头结点作为Top,在每次入栈时,就把新的元素加到原头结点的前面,使其成为新的头结点。链表模拟的堆栈在入栈时,不需要考虑是否满栈,因为链表可以无限延长;而数组模拟的链表在入栈时需要判断是否满栈,因为数组的大小时固定的。


利用表达式求值时,如果遇到右括号,则右括号不需要入栈,而是作为标识符。


<2> 队列的顺序存储

用链表模拟队列,应该以头结点作为front


3、多项式的加法
在这里插入图片描述
其中Rear是指针,使用&Rear传递的原因是,函数是值传递,因此要改变Rear的值,必须传递它的地址。


4、多项式乘法的思路

<1> 设有两个多项式P1,P2,将P1的第一项乘以P2,储存在链表中
<2> 往后的P1的若干项,先与P2的某一项相乘,然后加到储存的链表中。在插入链表时,需判断指数的大小关系。假设多项式指数是递减,那么需要找到当插入的结点的指数刚好不大于某一结点的位置,此时再判断指数是否相等;若相等,再判断系数相加后是否为0,若为0,则将该节点删除,若不为0,则合并该结点。若不相等,则说明小于该结点的指数,此时只需要插入结点即可。
<3> 完成后将结点的头空结点删除,返回头结点即可。


三、树

1、判定树
n个结点的判定树深度为[log2(N)]+1,深度的数代表查询几次。


2、树的定义
在这里插入图片描述
在这里插入图片描述


3、树的基本术语
在这里插入图片描述
在这里插入图片描述


4、树的表示
在这里插入图片描述
一般的树,可以用儿子-兄弟表示法,采用链表的结构将他们表示出来。
像结点最多有两个子树,即结点的度为2,树的度也为2,这种树称之为二叉树。二叉树跟一般的度为2的树的区别就是:二叉树有左右之分。


5、二叉树

<1> 二叉树的定义
在这里插入图片描述
<2> 特殊的二叉树

满二叉树的叶结点必须是在同一深度
在这里插入图片描述
在这里插入图片描述

<3> 二叉树的重要性质
在这里插入图片描述
关于第三个结论的证明:(先假设n1为度为1的结点个数)
根据二叉树的性质可知:总边数 = 结点数n - 1 = n1 + n2 + n0 - 1
由上至下观察二叉树,可知 总边数 = 0×n0 + 2×n2 + 1×n1
所以可知:n0 = n2 + 1


<4> 二叉树的存储结构

① 顺序存储结构

用数组存储二叉树,最方便被存储的二叉树类型是完全二叉树。将完全二叉树的每个结点从上至下、从左至右进行编号,然后将对应编号放入数组中。于是便可以得到以下关系:
在这里插入图片描述
一般的二叉树如果用数组存储,只需要将这种二叉树补齐成完全二叉树进行存储即可,被补齐的结点在数组留下空位。但是这种操作会造成数组空间的浪费。


② 链表存储结构

一般的二叉树可以使用儿子-兄弟链表结构来进行存储。


<5> 二叉树的遍历

① 先序中序后序遍历

遍历的原则:每遇到一个结点,就将其作为根结点进行遍历。遇到叶结点时直接访问。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


另,中序遍历的非递归遍历:

二叉树的非递归遍历一般用堆栈实现。
在这里插入图片描述


根据遍历顺序的定义,可以知道先序遍历的非递归写法:
在这里插入图片描述


若用堆栈的方式实现后序遍历,则原理是:若右子树为空,则访问该结点。但由于这样会造成死循环,所以应该用一个指针来记录上一次访问的结点。若此时访问的结点与上次访问的结点相同,说明此结点的根结点左右子树均已遍历,应访问该根结点。


② 层序遍历

层序遍历可以利用队列来实现。原理是:将结点入队,循环地令结点出队,完成遍历,再把它的左右子结点入队。
在这里插入图片描述
通过这样的操作,就能实现二叉树一层一层的遍历。


<6> 二叉树遍历的应用
在这里插入图片描述


在这里插入图片描述
二元运算表达式树的原理是:叶结点为数据,其余为运算符,当某结点作为根结点时,形式为:左子节点 根结点 右子节点。

利用中序遍历输出时,若想不受运算符优先级的影响,则需要在第一次输出左子树时输出左括号,在左子树为空时输出右括号。


在这里插入图片描述


<7> 二叉树的同构

同构的定义:在这里插入图片描述
判断是否同构:
在这里插入图片描述


<8> 二叉搜索树
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


<9> 平衡二叉树

平衡二叉树是二叉搜索树的一类
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
另,平衡二叉树的调整:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
<10> 判断是否为同一二叉搜索树
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


PS:线性结构习题——逆转单链表
在这里插入图片描述


四、堆

1、什么是堆
在这里插入图片描述
可以用完全二叉树来表示堆:
在这里插入图片描述
在这里插入图片描述


<2> 堆的插入
在这里插入图片描述
在这里插入图片描述


<3> 堆的删除

堆的删除,就是删除根结点的元素。因此有以下步骤:

① 将最后一个元素与第一个元素替换,此时根结点的左右子树均为堆
② 将根结点与左右子树结点进行比较,找出较大的结点,再与根结点比较。若根结点比它大,则不必替换;若根结点比它小,则与它进行交换后再以其为根结点进行相同的交换步骤,直至各个结点有序。
在这里插入图片描述
<4> 堆的建立
在这里插入图片描述

堆的建立与堆的删除中的重要步骤原理一致

构建成完全二叉树后,找到最后一个仅有一个儿子的结点,以此为基准,对其以前的每个结点的子树进行大小替换,使其变成堆。待到除根结点以外的所有结点变成堆后,接下来的操作便和堆的删除后调整其有序性的操作完全一致。


五、哈夫曼树与哈夫曼编码

1、哈夫曼树的定义
在这里插入图片描述


2、哈夫曼树的构造
在这里插入图片描述
在这里插入图片描述
3、哈夫曼编码
在这里插入图片描述
在这里插入图片描述

六、集合及运算

1、集合的表示
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
2、集合运算
在这里插入图片描述
在这里插入图片描述
由于合并后会增加树的高度导致查找函数效率降低,因此在合并前需要比较一下根结点元素的个数,将高度较小的数并入高度较大的树,防止查找效率降低
在这里插入图片描述
负数代表根结点,负数的绝对值代表元素个数。这是一个比较巧妙的表示方法。
在这里插入图片描述

七、堆中的路径

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

八、File Transfer

1、按秩归并
当合并两个集合时,需要把小的树往大的树合并。实现这种操作由两种方法:
① 记录树的高度并比较
② 记录树的规模(结点个数)并比较


2、路径压缩
在这里插入图片描述

九、图

1、什么是图
<1> 定义
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
有关图论的常见术语非常多,所以这里只介绍最常见的三个概念


<2> 邻接矩阵表示法
在这里插入图片描述
该邻接矩阵的特点是:
① 对角线上的元素都为0(因为不存在自回路)
② 矩阵是对称的(因为是无向图)

在这里插入图片描述
在这里插入图片描述
出度入度的情况存在于有向图

在这里插入图片描述
完全图是指任意两个顶点都存在边的图。
浪费时间是因为稀疏图中的边很少,而要知道稀疏图有多少条边,就需要扫描一遍稀疏图


<3> 邻接表表示法
在这里插入图片描述
在这里插入图片描述
对有向图来说,若要计算“入度”,可以构造指向矩阵的列的数组,来实现计算“入度”


2、图的遍历
<1> DFS
在这里插入图片描述
用邻接表存储图,遍历顶点时,需要访问N个顶点,E条边,所以是O(N+E);
用邻接矩阵存储图,遍历顶点时,每访问一个顶点,就要访问访问一行或一列,即N个元素,所以是O(N^2);


<2> BFS
BFS类似于树的层序遍历

用邻接表和邻接矩阵存储图时,使用BFS遍历方式的时间复杂度和使用DFS时相同,原理也一样


<3> 为什么需要两种遍历
BFS擅长解决最小问题
DFS擅长解决遍历或者所有问题


<4> 图连不通怎么办
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
弱连通图:将有向图的有向边替换为无向边,得到的图成为基图;若有向图的基图是连通的,则该有向图成为弱连通图


3、最短路径问题
<1> 概述
在这里插入图片描述
在这里插入图片描述


<2> 无权图的单源最短路
在这里插入图片描述
在这里插入图片描述
path[W] = V(V为W的前一个顶点)


<3> 有权图的单源最短路
在这里插入图片描述
存在负值圈的有权图不适用大多数算法
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


<4> 多源最短路算法
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
利用path二维数组记录顶点位置,便于打印最短路径。实现方法是:递归地调用函数先打印出i到k的路径,再打印k到j的路径。


4、最小生成树问题
<1> Prim算法
最小生成树存在和图连通是充分必要关系
在这里插入图片描述
在这里插入图片描述
Prim算法和Dijkstra算法类似,左是Dijkstra算法,右是Prim算法
该算法处理稠密图时效果较佳
Prim算法主要关注于顶点
在这里插入图片描述


<2> Kruskal算法
原理:不断收录最小边,当构成最小生成树时停止
Kruskal算法主要关注于边
在这里插入图片描述
5、拓扑排序
<1> 拓扑排序
在这里插入图片描述
在这里插入图片描述
以下是经过拓扑排序之后的拓扑序:
在这里插入图片描述
以下是实现拓扑排序的伪代码:
处理未输出的入度为0的顶点时,O(|V|)的算法是效率不高的
在这里插入图片描述
以下是处理未输出的入度为0的顶点时较好的方法:
DAG为有向无环图
在这里插入图片描述
<2> 关键路径
在这里插入图片描述
虚线的存在是因为:要执行6、7,就必须等待4、5完成,因此将5连接一条虚线到4,形成一个小集合。
在这里插入图片描述
3、Dijkstra算法的应用
<1> 数最短路径数问题:
① 初始化:由起点到结点V的最短路径数为1条
② 找到更短路时:起点到V的邻接点W的最短路径数跟到V的最短路径数相同
③ 找到等长路时:起点到V的邻接点W的距离跟到V的距离相同,那么最短路径数就是起点到V的路径数+起点到W的路径数

<2> 求边数最少的最短路
① 初始化:从起点到自己的边为0,所以初始化为0
② 找到更短路或等长路时:边数可以理解为权重为1的边,在处理时比较方便。此时该结点的边数+1
在这里插入图片描述

十、排序

1、简单排序
<1> 冒泡排序的优化
可以用一个标记flag来判断在某次循环时是否发生数据的交换,若无,则说明此时是有序的,因此可以接下来的循环可以不用进行,直接退出即可。
在这里插入图片描述
<2> 插入排序
在这里插入图片描述


<3> 时间复杂度下界
在这里插入图片描述
在这里插入图片描述


2、希尔排序
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


3、堆排序
<1> 选择排序在这里插入图片描述
以上是实现选择排序的伪代码。其中要实现ScanForMin()函数,需要快速找到最小元,可以用最小堆来实现。于是就引出了堆排序。


<2> 堆排序
堆排序适用于队列中存在优先级的情况
在这里插入图片描述
在这里插入图片描述
算法2的实现代码:

#include <iostream>
#include <algorithm>
using namespace std;
 
void max_heapify(int arr[], int start, int end) 
{
    //建立父节点指标和子节点指标
    int dad = start;
    int son = dad * 2 + 1;
    while (son <= end)  //若子节点指标在范围内才做比较
    {    
        if (son + 1 <= end && arr[son] < arr[son + 1]) //先比较两个子节点大小,选择最大的
            son++;
        if (arr[dad] > arr[son]) //如果父节点大於子节点代表调整完毕,直接跳出函数
            return;
        else  //否则交换父子内容再继续子节点和孙节点比较
        {
            swap(arr[dad], arr[son]);
            dad = son;
            son = dad * 2 + 1;
        }
    }
}
 
void heap_sort(int arr[], int len) 
{
    //初始化,i从最後一个父节点开始调整,len/2-1即为最后一个父节点
    for (int i = len / 2 - 1; i >= 0; i--)
        max_heapify(arr, i, len - 1);
    //先将第一个元素和已经排好的元素前一位做交换,再从新调整(刚调整的元素之前的元素),直到排序完毕
    for (int i = len - 1; i > 0; i--) 
    {
        swap(arr[0], arr[i]);
        max_heapify(arr, 0, i - 1);
    }
}
 
int main() 
{
    int arr[] = { 3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6 };
    int len = (int) sizeof(arr) / sizeof(*arr);
    heap_sort(arr, len);
    for (int i = 0; i < len; i++)
        cout << arr[i] << ' ';
    cout << endl;
    system("pause");
    return 0;
}

4、归并排序
归并排序是稳定的排序算法,时间复杂度为O(nlogn)
归并排序一般用于外排序而非内排序
<1> 有序子列的归并
在这里插入图片描述
对于两段有序子序列,可以使用归并排序的算法:

/* A[]是存放两段有序子序列的数组,TmpA[]是存放排序后数据的数组,
L是左子序列的起始位置,R是右子序列的起始位置,RightEnd是右子序列的终点位置 */
void Merge(int A[], int TmpA[], int L, int R, int RightEnd)
{
    int LeftEnd = R - 1;                // 左子序列的终点位置就是右子序列的起始位置的前一个
    int Tmp = L;                        // 新数组的起始位置和左子序列的起始位置一样
    int NumElements = RightEnd - L + 1; // 数组的总个数
    /* 以下操作类似于 多项式相加  */
    /* 如果左边或右边的子序列还没到达它的终点位置 */
    while ( L <= LeftEnd && R <= RightEnd ) {
        if ( A[L] <= A[R] ) TmpA[Tmp++] = A[L++]; // 赋值后指针向后移一位
        else                TmpA[Tmp++] = A[R++];
    }
    /* 如果有其中之一的子序列还没扫描完 */
    while ( L <= LeftEnd  )  TmpA[Tmp++] = A[L++];
    while ( R <= RightEnd )  TmpA[Tmp++] = A[R++];
    /* 将排序后的序列复制给原来的数组 */
    for (int i=0; i<NumElements; i++, RightEnd--) {
        A[RightEnd] = TmpA[RightEnd];
    }
}

<2> 递归算法
统一函数接口的归并排序的递归算法:

/*/* A[]是存放两段有序子序列的数组,TmpA[]是存放排序后数据的数组,
L是左子序列的起始位置,R是右子序列的起始位置,RightEnd是右子序列的终点位置 */
void Merge(int A[], int TmpA[], int L, int R, int RightEnd)
{
    int LeftEnd = R - 1;                // 左子序列的终点位置就是右子序列的起始位置的前一个
    int Tmp = L;                        // 新数组的起始位置和左子序列的起始位置一样
    int NumElements = RightEnd - L + 1; // 数组的总个数
    /* 以下操作类似于 多项式相加  */
    /* 如果左边或右边的子序列还没到达它的终点位置 */
    while ( L <= LeftEnd && R <= RightEnd ) {
        if ( A[L] <= A[R] ) TmpA[Tmp++] = A[L++]; // 赋值后指针向后移一位
        else                TmpA[Tmp++] = A[R++];
    }
    /* 如果有其中之一的子序列还没扫描完 */
    while ( L <= LeftEnd  )  TmpA[Tmp++] = A[L++];
    while ( R <= RightEnd )  TmpA[Tmp++] = A[R++];
    /* 将排序后的序列复制给原来的数组 */
    for (int i=0; i<NumElements; i++, RightEnd--) {
        A[RightEnd] = TmpA[RightEnd];
   /* 由于不知道起始位置,而终点位置是已知的,因此可以从右往左进行赋值 */
    }
}
void MSort(int A[], int TmpA[], int L, int RightEnd)
{
    int Center = (L+RightEnd) / 2;
    if ( L < RightEnd ) {
        MSort(A,TmpA,L,Center);
        MSort(A,TmpA,Center+1,RightEnd);
        Merge(A,TmpA,L,Center+1,RightEnd);
    }
}
void Merge_sort(int A[], int N)
{
 /* 临时数组不能在进行排序操作时建立,否则会进行多次申请释放空间 */
    int *TmpA = (int*)malloc(N*sizeof(int));
    if ( TmpA != NULL ) {
        MSort(A,TmpA,0,N-1);
        free(TmpA);
    }
    else    puts("Insufficient Space!");
}

<3> 非递归算法
实现原理:先把每个元素看成长度为1的有序子序列,归并之后,有序子序列长度增加,再进行循环归并。
Merge1和Merge的区别是前者不需要从TmpA归并到A
在这里插入图片描述
统一接口函数:
在这里插入图片描述
5、快速排序
<1> 算法概述
在这里插入图片描述
每次递归时中分的情况时最快的,时间复杂度为O(NlogN)


<2> 主元的选取
主元的选取很关键。
在这里插入图片描述
在这里插入图片描述
<3> 子集划分
在这里插入图片描述
<4> 算法实现

int GetPivot(int A[], int Left, int Right) 
{
	int Center = (Left+Right) / 2;
	if ( A[Left]   > A[Center] )	swap(A[Left],A[Center]);
	if ( A[Left]   > A[Right]  )	swap(A[Left],A[Right]);
	if ( A[Center] > A[Right]  )	swap(A[Center],A[Right]);
	swap(A[Center],A[Right-1]);
	return A[Right-1];
}
void Qsort(int A[], int Left, int Right)
{
	if ( Right-Left+1 < 2 )	return;
	int Pivot = GetPivot(A,Left,Right);
	int i = Left;
	int j = Right - 1;
	while ( true ) {
		while ( A[++i] < Pivot );
		while ( A[--j] > Pivot );
		if ( i < j ) {
			swap(A[i],A[j]);
		}
		else	break;
	}
	swap(A[i],A[Right-1]);
	Qsort(A,Left,i-1);
	Qsort(A,i+1,Right);
}
void quick_sort(int A[], int N)
{
	Qsort(A,0,N-1);
}

在这里插入图片描述


6、表排序
<1> 算法概述
在这里插入图片描述
<2> 物理排序
在每次遍历环的时候就让table[i]=i,这样判断环结束时就比较方便
在这里插入图片描述
在这里插入图片描述


7、基数排序
<1> 桶排序
在这里插入图片描述


<2> 基数排序
在这里插入图片描述
<3> 多关键字的排序
在这里插入图片描述
在这里插入图片描述
答案是否,哪个可能性较多,就以哪个为基准建桶。


8、排序算法的比较
在这里插入图片描述


十一、散列查找

1、散列表
<1> 散列的基本思路
例子:编译处理时,涉及变量及属性的管理:
① 插入:新变量定义
② 查找:变量的引用
于是,这个问题也就是 动态查找问题

假设用搜索树进行变量的管理,两个变量名比较的效率不高,无法做到像整数那样只比较一次。因此可以把变量名转换成数字,再进行比较。这就是散列查找的思想。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
<2> 什么是散列表
在这里插入图片描述
在这里插入图片描述
设计这样的散列函数,在不考虑冲突的情况下,函数值共有26种情况,再考虑发生冲突情况,所以创建一个26*2的二维数组来存储这些字符串。
在这里插入图片描述
在这里插入图片描述
2、散列函数的构造方法
<1> 数字关键词的散列函数构造
在这里插入图片描述
在这里插入图片描述
p取质数的原因是:这样的因子更少,分布广泛性得到提升。

atoi函数的作用是把字符串转换为数字
在这里插入图片描述
18位的身份证号码中,有6位的情况是比较随机的,因此取他们作为关键字来构造散列函数。
在这里插入图片描述
折叠法使数字经过散列函数后,分布更加广泛
平方取中法使数字不受低位数字的影响,分布更加广泛


<2> 字符串关键词的散列函数构造
在这里插入图片描述
显然1、2的方法都是有缺陷的。其中2的27进制是因为考虑到了空格。

要计算以下的式子,算法和 计算多项式 类似。
在这里插入图片描述
3、冲突处理方法
<1> 开放定址法
在这里插入图片描述
在这里插入图片描述
<2> 线性探测
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
ASLu的计算方法:根据余数的情况来计算,若冲突则往后移一位,如果遇到空,则说明找不到,此时停止并记录移动次数。除以11是因为对11取余,一共有11种情况。


<3> 平方探测法
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


<4> 平方探测法的实现
Info的作用是作为标识,来判断是否已经有存放元素
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
再散列时,所有元素的存放地址需重新计算。


<5> 分离链接法
在这里插入图片描述
在这里插入图片描述
4、散列表的性能分析
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


十二、综合习题选讲

1、Insert or Merge
在这里插入图片描述
要判断某一序列是否为归并排序的中间序列,可以用一个for循环来解决。例如,若要判断该归并段长度为l,则判断l/2处是否有序,若每个l/2处都有序,则说明该归并段长度为l。


2、Hashing-Hard Version
在这里插入图片描述
根据以上的思路,可以构造出各个元素的输入顺序层。在处理输出结果时,会发现其实这是拓扑排序。因此可以说明:在遇到有关元素的先后顺序问题时,可以想到拓扑排序。


3、串的模式匹配(KMP算法)
<1> 算法思路
在这里插入图片描述
match函数:寻找头尾相同的字符中前一段字符的最后一个字符的下标。


<2> 算法实现
在这里插入图片描述
<3> BuildMatch的编程实现
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值