【数据结构】04串

  • 1. 定义
  • 2. 串的比较
  • 3. 串的存储结构
  • 4. 具体实现
  • 5. 模式匹配
    • 5.1 常规思路实现
    • 5.2 KMP模式匹配算法
      • 5.2.1 next数组计算
      • 5.2.1 代码计算next数组
      • 5.2.2 KMP算法实现

1. 定义

串(string)是由零个或多个字符组成的有限序列,又叫字符串。
一般记为s= a 1 , a 2 , . . . , a n , ( n ≥ 0 ) a_1,a_2,...,a_n,(n\ge0) a1,a2,...,an,(n0)。串中字符数目n称为串的长度。零个字符的串称为空串。

2. 串的比较

给定两个串:s= a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an,t= b 1 , b 2 , . . . , b m b_1,b_2,...,b_m b1,b2,...,bm,当满足以下条件之一时, s < t s<t s<t
(1) n < m n<m n<m,且 a i = b i a_i=b_i ai=bi,例如: s = h a p , t = h a p p y s=hap,t=happy s=hap,t=happy,就有 s < t s<t s<t
(2)存在某个 k ≤ min ⁡ ( m , n ) k\le\min(m,n) kmin(m,n),使得 a i = b i a_i=b_i ai=bi a k < b k a_k<b_k ak<bk,就有 s < t s<t s<t。例如 s = h a p p e n s=happen s=happen t = h a p p y t=happy t=happy,此时 k = 4 k=4 k=4,且字符有: e < y e<y e<y,则 s < t s<t s<t

3. 串的存储结构

串的存储结构与线性表相同,分为两种:顺序存储结构和链式存储结构。

  1. 串的顺序存储结构使用一组地址连续的存储单元来存储传中的字符序列。
  2. 串的链式存储结构与线性表相似,但是如果一个字符占用一个结点,就会存在很大的空间浪费。
    串的链式存储结构除了在连接串与串操作时,有一定方便之外,总的来说不如顺序存储灵活,性能也不如顺序存储结构好。

对于串的顺序存储有一些变化,串值的存储空间可在程序执行过程中动态分配而得。

4. 具体实现

为了方便管理,利用C++的类实现了String:

#include <iostream>
using namespace std;

class String
{
	friend ostream& operator<<(ostream& os, const String& s); // 友元函数
private:
	int max_size = 10;
	int extend_size = 10;
	char* ptr; // 字符指针
	int len;

	// 扩展内存
	void ExtendString()
	{
		char* new_ptr = new char[max_size + extend_size];
		memset(new_ptr, 0, sizeof(char) * (max_size + extend_size));
		// 拷贝数据
		memcpy(new_ptr, ptr, sizeof(char) * max_size);
		// 清空内存
		delete[] ptr;
		// 指向新内存
		ptr = new_ptr;
		// 更新最长大小
		max_size = max_size + extend_size;
	}

public:
	String()  // 构造函数
	{
		// cout << "调用了默认构造函数" << endl;

		ptr = new char[max_size]; // 初始化内存
		memset(ptr, 0, sizeof(char) * max_size);
		len = 0; // 字符串长度
	}

	String(const char* s) // 构造函数
	{
		 cout << "调用了构造函数" << endl;

		int len = strlen(s);
		ptr = new char[this->max_size]; // 初始化内存
		memset(ptr, 0, sizeof(char) * this->max_size);
		while (this->max_size < len)
		{
			ExtendString();
		}
		// 拷贝数据
		memcpy(this->ptr, s, sizeof(char) * len);
		this->len = len;
	}

	String(const String& s) // 构造函数
	{
		 cout << "调用了拷贝构造函数" << endl;

		ptr = new char[s.max_size]; // 初始化内存
		memset(ptr, 0, sizeof(char) * s.max_size);
		// 拷贝数据
		memcpy(ptr, s.ptr, sizeof(char) * s.len); // 拷贝字符串
		len = s.len;
		max_size = s.max_size;
	}

	String& operator=(const char* str) // 赋值函数
	{
		 cout << "调用了重载的赋值函数char*" << endl;

		int len = strlen(str);
		while (strlen(str) > this->max_size)
		{
			ExtendString();
		}
		memcpy(this->ptr, str, sizeof(char) * len);
		this->len = len;
		return *this;
	}

	String& operator=(const String& str) // 赋值函数
	{
		 cout << "调用了重载的赋值函数String" << endl;

		while (str.max_size> this->max_size)
		{
			ExtendString();
		}
		memcpy(this->ptr, str.ptr, sizeof(char) * str.len);
		this->len = str.len;
		return *this;
	}

	// 清空字符串
	void clear() 
	{
		memset(ptr, 0, sizeof(char)*max_size); // 内存置0
		len = 0;
	}

	// 返回字符串长度
	int length()const 
	{
		return len; 
	}

	// 返回从第pos个字符开始,长度为len的子串
	String sub(int pos, int len) 
	{
		// 创建临时对象
		String tmp;
		for (int i = 0; i < len; i++)
		{
			tmp.ptr[i] = this->ptr[i + pos - 1];
		}
		tmp.len = len;
		return tmp;
	}
	// 将str插入到当前字符第pos个字符之后
	String Insert(int pos, const String& str)
	{
		int whole_len = str.len + this->len;
		while (this->max_size < whole_len)
		{
			ExtendString();
		}
		// 插入字符
		// 1.保存第pos个字符之后的所有字符,后移str_len位 即:pos-1开始的len-pos个字符移动到pos+str_len-1
		for (int i = this->len; i > pos; i--)
		{
			this->ptr[i + str.len - 1] = this->ptr[i - 1]; // 
		}
		// 2.插入字符从 str.ptr[0]到 str.ptr[len-1] 到pos-1至pos+str.len-1;
		memcpy(&(this->ptr[pos]), &(str.ptr[0]), sizeof(char) * str.len);
		this->len = whole_len;
		return *this;
	}

	~String()
	{
		delete[] ptr;
		ptr = nullptr;
		len = 0;
	}

	bool operator<(const String& str)const
	{
		int i = 0;
		for (i = 0; i < this->len && i < str.len; i++)
		{
			if (this->ptr[i] == str.ptr[i])
			{
				continue;
			}
			if (this->ptr[i] > str.ptr[i]) // 前面都相等,一旦有一个字符大,则整个字符串都大
			{
				return false;
			}
			else // 前面都相等,当前字符小
			{
				return true;
			}
		}
		if (this->len < str.len)// 退出比较的原因是前面字符都相等,但当前字符串的字符较短
		{
			return true;
		}
		return false;
	}

	bool operator>(const String& str)const
	{
		int i = 0;
		for (i = 0; i < this->len && i < str.len; i++)
		{
			if (this->ptr[i] == str.ptr[i])
			{
				continue;
			}
			if (this->ptr[i] < str.ptr[i]) // 前面都相等,一旦有一个字符小,则整个字符串都小
			{
				return false;
			}
			else // 前面都相等,当前字符大
			{
				return true;
			}
		}
		if (this->len > str.len)// 退出比较的原因是前面字符都相等,但当前字符串的字符更长
		{
			return true;
		}
		return false;
	}
};

// 重载输出
ostream& operator<<(ostream& os, const String& s)
{
	for (int i = 0; i < s.len; i++)
	{
		os << s.ptr[i];
	};
	return os;
}


int main(void)
{

	String s1;
	s1 = "ace";
	String s2 = s1;
	String s3("bdf");
	String s4 = "asw";  // 自动类型转换,调用赋值函数
	/*s3 = s2.sub(1, 2);*/
	
	cout << s1 << endl;
	s1.clear();
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;

	cout <<( s3 < s2) << endl;
	s2.Insert(3, s3);
	cout << s2 << endl;
	return 0;
}

5. 模式匹配

在一段字符串中去定位子串的位置的操作称作串的模式匹配,这是串中最重要的操作之一。
假设我们要从主串"S=goodgoogle"中,找到子串"T=google"的位置。通常需要进行下面的步骤:

  1. 主串S第一位开始,S与T的前三个字符都匹配成功,但S的第四个字符为"d"而T的第四个字符为"g"。第一位匹配失败。
  2. 主串S第二位开始,S首字符为"o"而T的首字符为"g",第二位匹配失败。
  3. 同理,第三位和第四位都匹配失败
  4. 主串第五位开始,S与T,6个字母全匹配,匹配成功。

简单来说,对主串的每个字符作为子串开头,与要匹配的子串进行匹配。对主串做外部循环,每个字符开头做子串T长度的内部循环,直到匹配成功或主串遍历完成为止。

5.1 常规思路实现

		int Index(const String& T)
	{
		int i, j = 0;
		for (i = 0; i < this->len; i++)
		{
			for (j = 0; j < T.len; j++)
			{
				if (this->ptr[i+j] == T.ptr[j]) // 主串以i开头的T长度的子串匹配
				{
					continue;
				}
				break;
			}
			if (j == T.len) // 子串匹配成功
			{
				return i; // 返回此时子串在主串中的位置
			}
		}
		return -1; // 匹配失败
	}

5.2 KMP模式匹配算法

常规思路的匹配算法需要挨个遍历主串和子串,效率低效。KMP算法的思路是当发现某一个字符不匹配的时候,由于已经知道之前遍历过的字符,利用这些信息避免暴力算法中"回退"的步骤。
KMP算法的原理:
1)在匹配过程中,主串的指针不需要回溯,只回溯子串的指针。
2)如果子串和主串中前n个字符匹配成功,遇到匹配失败的字符时,子串回溯的下标由子串的内容决定(回溯到:匹配失败前,子串内容最长相等前后缀 的长度),然后继续比较。
前缀:包含首位字符但不包含末位字符的子串。如:ababa的前缀包括:a,ab,aba,abab
后缀:包含末位字符但不包含首位字符的子串。如:ababa的后缀包括:a,ba,aba,baba。
则对于字符串:ababa最长公共前后缀为:aba。
具体流程:依次匹配主串和子串的字符,当遇到子串与主串字符不匹配时,子串指针回溯,并从回溯的位置继续与主串当前位置字符比较。如图中所示:ABABABCAA与ABABC匹配,当遇到主串字符A时,子串字符为C,此时无法匹配,子串指针回溯到第3个字符即A处,继续比较。KMP算法的关键是如何获取子串回溯位置,即next数组。
在这里插入图片描述

5.2.1 next数组计算

对于子串:ABABC
第一个字符A,没有前缀没有后缀,对应的next数组值为0。
第二个字符AB,包含的前后缀有:A B 对应的next数组值为1。
第三个字符的子串为ABA,包含的前后缀有:A A AB BA ,最长的长度为1。则next数组值为1。
第四个字符的子串为ABAB,包含的前后缀有:A B AB AB ABA BAB,最长的公共前后缀长度为2。next数组值为2。
第五个字符的子串为ABABC,包含的前后缀有:A C AB BC ABA ABC ABABA BABC,最长的公共前后缀长度为0。next数组值为0。
因此对于ABABC的next数组值为:0,1,1,2,0。

对于子串:AABAAF计算next数组:
初始化:i 和 j 其中i指向的是后缀末尾,j指向的是前缀末尾即把S[0,j]看作是子串,把S[1,i]看作是主串来理解。初始时j=0,i=1;next[0]=0。
前后缀不同的情况:S[i]!=S[j],此时需要查询next[j-1]的值,查询到j需要回退的位置,必须要满足j>0,直到
S[i]==S[j]或者回退到初始位置。
前后缀相同的情况:S[i]==S[j],j++。
更新next数组:next[i]=j。
模拟运行:

  • 初始化:next[0] = 0. i =1 ,j =0.
  • i=1,j=0.S[0]=A==S[1]=A => j++,j=1. next[1]=1.
  • i=2,j=1;S[1]=A != S[2]= B => j=next[j-1]=next[0]=0 ,S[0]!=S[2] j>0. next[2] = 0.
  • i=3,j=0;S[0] =A ==S[3]=A => j++,j=1. next[3] = 1.
  • i=4,j=1;S[1]=A == S[4]= A => j++,j=2. next[4] = 2.
  • i=5,j=2;S[2]=B!=S[5]=F =>j = next[j-1]=next[1] = 1 ,S[1]!=S[5] j=next[j-1]=next[0]=0 j>0.next[5]=0.
    在这里插入图片描述

5.2.1 代码计算next数组

	// 获取next数组
	int* getNext()
	{
		// 创建next数组,长度与字符串长度一致
		delete[] next;
		next = new int[this->len];
		memset(next, 0, sizeof(int) * this->len);

		// 初始化
		int i, j = 0;
		next[0] = 0;
		for (i = 1; i < this->len; i++)
		{
			// 前后缀不相同的情况
			while (j > 0 && ptr[i] != ptr[j])
			{
				// 回溯j
				j = next[j - 1];
			}
			// 前后缀相同的情况
			if (ptr[i] == ptr[j])
			{
				j++;
			}
			// 更新next
			next[i] = j;
		}
		return next;
	}

5.2.2 KMP算法实现

/ KMP算法
	int KMP(String& T)
	{
		// 获取子串next数组
		int* next_ptr = T.getNext();
		// 开始匹配
		int i = 0, j = 0;
		while (i < len)
		{
			if (ptr[i] == T.ptr[j]) // 匹配成功
			{
				i++; j++;
			}
			else if (j > 0) // 匹配失败,子串指针回溯,并且需要保证回溯位置不越界(即无法回溯到首个字符前)
			{
				j = next_ptr[j - 1];
			}
			else // 子串第一个字符就匹配失败
				i++;
			if (j == T.len)// 子串已经到达末尾,即全部匹配上
			{
				return i - j; // 返回位置
			}
		}
		return -1; // 匹配失败
	}

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

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

相关文章

软件供应链安全:寻找最薄弱的环节

在当今的数字时代&#xff0c;软件占据主导地位&#xff0c;成为全球组织业务和创新的支柱。它是差异化、项目效率、成本降低和竞争力背后的驱动力。软件决定了企业如何运营、管理与客户、员工和合作伙伴的关系&#xff0c;以及充分利用他们的数据。 挑战在于&#xff0c;当今…

【MVCC】深入浅出彻底理解MVCC

MVCC概述 MVCC&#xff08;Multi-Version Concurrency Control&#xff09;即多版本并发控制。主要是为了提高数据库的并发性能而提供的&#xff0c;采用了不加锁的方式处理读-写并发冲突&#xff0c;确保了任何时刻的读操作都是非阻塞的。只需要很小的开销&#xff0c;就可以…

电视盒子哪个好?2024口碑网络电视盒子排行榜

多年来电视盒子始终占据重要地位&#xff0c;功能上并没有受到影响。在这么多品牌中哪些电视盒子的评价是最好的呢&#xff1f;小编根据各大电商平台的用户评价情况整理了口碑最好的网络电视盒子排行榜&#xff0c;跟着小编一起看看市面上的电视盒子哪个好吧。 TOP 1&#xff1…

如何访问远程MySQL数据库?

远程访问MySQL数据库是在不同设备之间实现数据交互的一种方式。通过远程访问&#xff0c;用户可以轻松地操作远程MySQL数据库&#xff0c;从而实现数据的读写、修改和查询等操作。本文将介绍远程访问MySQL数据库的原理和实现方法&#xff0c;以及一种被广泛应用的解决方案【天联…

[入门到放弃]设计模式-笔记

模块化设计 20240448 模块不包含数据&#xff0c;通过实例的指针&#xff0c;实现对实例的操作&#xff1b;唯一包含的数据是用于管理这些模块的侵入式链表模块只负责更具定义的数据结构&#xff0c;执行对应的逻辑&#xff0c;实现不同实例的功能&#xff1b; 参考资料 使用…

TCP/IP协议—UDP

TCP/IP协议—UDP UDP协议UDP通信特点 UDP头部报文UDP检验 UDP协议 用户数据传输协议 (UDP&#xff0c;User Datagram Protocol) 是一种无连接的协议&#xff0c;提供了简单的数据传输服务&#xff0c;不保证数据的顺序以及完整性。应用层很多通信协议都基于UDP进行传输&#x…

[当人工智能遇上安全] 13.威胁情报实体识别 (3)利用keras构建CNN-BiLSTM-ATT-CRF实体识别模型

《当人工智能遇上安全》系列将详细介绍人工智能与安全相关的论文、实践&#xff0c;并分享各种案例&#xff0c;涉及恶意代码检测、恶意请求识别、入侵检测、对抗样本等等。只想更好地帮助初学者&#xff0c;更加成体系的分享新知识。该系列文章会更加聚焦&#xff0c;更加学术…

最新常见的图数据库对比,选型,架构,性能对比

图数据库排名 地址&#xff1a;https://db-engines.com/en/ranking/graphdbms 知识图谱查询语言 SPARQL、Cypher、Gremlin、PGQL 和 G-CORE 语法 / 语义 / 特性SPARQLCypherGremlinPGQLG-CORE图模式匹配查询语法CGPCGPCGP(无可选)1CGPCGP语义子图同态、包 2无重复边、包 2子…

大唐极简史

唐朝&#xff08;618年-907年&#xff09;&#xff0c;是中国历史上一个强盛的朝代&#xff0c;以其疆域辽阔、政治稳重、经济繁荣、文化繁荣而著称。唐朝的开国皇帝是李渊&#xff0c;他建立了一个强大的中央集权政府&#xff0c;使得唐朝成为当时世界上最繁荣的国家之一。 唐…

vmware虚拟机补救

更新了虚拟机里面工具和资料&#xff0c;进行了磁盘整理和压缩&#xff0c;虚拟机运行进不去系统了。 网站找的修复方法均不可行。补救措施&#xff1a;利用DiskGenius.exe&#xff08;要用高版本不然复制的时候就知道了&#xff09; DG1342.rar - 蓝奏云 加载虚拟硬盘 2008x…

最新微信支付商家转账到零钱各场景怎样开通?

商家转账到零钱是什么&#xff1f; 商家转账到零钱是企业付款到零钱的升级版&#xff0c;它的功能是&#xff0c;如果系统需要对用户支付费用&#xff0c;比如发放佣金、提成、退款等&#xff0c;可以直接转账到用户的微信零钱。这个功能在 2022 年 5 月 18 日之前叫做企业付款…

2024年MathorCup数模竞赛B题问题一二三+部分代码分享

inputFolderPath E:\oracle\images\; outputFolderPath E:\oracle\process\; % 获取文件夹中所有图片的文件列表 imageFiles dir(fullfile(inputFolderPath, *.jpg)); % 设置colorbar范围阈值 threshold 120; % 遍历每个图片文件 for i 1:length(imageFiles) % 读…

个人网站制作 Part 18 提升用户体验:添加页面404错误处理 | Web开发项目

文章目录 &#x1f469;‍&#x1f4bb; 基础Web开发练手项目系列&#xff1a;个人网站制作&#x1f680; 添加404错误处理页面&#x1f528;创建404错误处理页面&#x1f527;步骤 1: 在项目根目录创建404错误处理页面&#x1f527;步骤 2: 添加404错误处理页面内容 &#x1f…

数据结构复习指导之绪论(数据结构的基本概念)

文章目录 绪论&#xff1a; 考纲内容 知识框架 复习提示 1.数据结构的基本概念 1.1基本概念和术语 1.数据 2.数据元素 3.数据对象 4.数据类型 5.数据结构 1.2数据结构三要素 1.数据的逻辑结构 2.数据的存储结构 3.数据的运算 绪论&#xff1a; 考纲内容 算法时…

计算机网络----第十天

配置vlan 广播风暴的含义&#xff1a; 含义&#xff1a;设备发出的广播帧在广播域中传播&#xff0c;占用网络带宽&#xff0c;降低设备性能 隔离广播的方式&#xff1a; 方式&#xff1a;用路由器来隔离广播 用VLN隔离广播 vlan的定义&#xff1a; 定义&#xff1a;虚拟…

13-pyspark的共享变量用法总结

目录 前言广播变量广播变量的作用 广播变量的使用方式 累加器累加器的作用累加器的优缺点累加器的使用方式 PySpark实战笔记系列第四篇 10-用PySpark建立第一个Spark RDD(PySpark实战笔记系列第一篇)11-pyspark的RDD的变换与动作算子总结(PySpark实战笔记系列第二篇))12-pysp…

海外媒体宣发:这7个旅游业媒体套餐击中宣传痛点-华媒舍

旅游业如何有效宣传自身成为了一个重要的问题。众多旅游企业却面临着宣传痛点&#xff0c;即无法将优质内容传达给目标受众。近期&#xff0c;出现了7个旅游业媒体套餐&#xff0c;它们被称为解决宣传痛点的突破性方案。本文将介绍这7个旅游业媒体套餐&#xff0c;并说明它们为…

一篇实操vitrualbox虚拟机根目录扩容(详细,实操陈功)

一篇实操vitrualbox虚拟机根目录扩容&#xff08;详细&#xff0c;实操陈功&#xff09; 创建虚拟介质 第一步、 第二步、 第三步、一直下一步&#xff0c;直到下面的页面 分配空间到更目录下 第一步、 第二步、查看创建的物理磁盘 lsblk第三步、查看原本磁盘可用空间 df …

掌握现代 C++:Lambda 在 C++14、C++17 和 C++20 中的演变

深入研究Lambda 在 C14、C17 和 C20 中的演变 一、背景二、C14 中的 Lambda2.1、默认参数2.2、模板参数2.3、广义捕获2.4、从函数返回 lambda 三、C17 中的 Lambda四、C20 中的 Lambda总结 一、背景 Lambda 是现代 C 最受欢迎的功能之一。自从在 C 11 中引入以来&#xff0c;它…

Day 23 669. 修剪二叉搜索树 108.将有序数组转换为二叉搜索树 538.把二叉搜索树转换为累加树 总结篇

修剪二叉搜索树 给定一个二叉搜索树&#xff0c;同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树&#xff0c;使得所有节点的值在[L, R]中 (R>L) 。你可能需要改变树的根节点&#xff0c;所以结果应当返回修剪好的二叉搜索树的新的根节点。 ​ 最直接的想法&#xff0…