高阶数据结构之哈希表基础讲解与模拟实现

news2025/1/12 10:01:39

程序猿的读书历程:x语言入门—>x语言应用实践—>x语言高阶编程—>x语言的科学与艺术—>编程之美—>编程之道—>编程之禅—>颈椎病康复指南。

前言:

哈希表(Hash Table)是一种高效的键值对存储数据结构,广泛应用于各种需要快速查找的场景,如数据库索引、缓存系统、集合等。它的基本思想是通过哈希函数将键映射到哈希表中的一个位置,从而实现快速的数据插入、删除和查找操作。下面我们将详细介绍哈希表的工作原理、实现方式、优缺点以及应用场景。

一、哈希概念

哈希是一种思想,普遍是通过一个哈希数组来存储数据的。学哈希思想,最重要的就是抓住映射两个字,它是一个无序的数据结构,所以想要找到存储的数据,就必须通过相对应的哈希关系来寻找。

对于该数据结构:
插入元素:
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素:
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功(一般来说的计算方法都是对值取余)
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称
为哈希表。
以最简单的整形数据来举例,哈希结构想要与整形产生映射关系,最简单的就是跟哈希数组的下标产生映射。如果存储数据的哈希数组大小为10,例如:数据集合{1,7,6,4,5,9},哈希函数设置为:hash(key) = key % capacity;(capacity为存储元素底层空间总的大小)
就通过把他们的值取余一个数组大小10,虽好放入相应下标的数组空间中,这就是最简单的映射关系:把1存储到下标为1的地方,4存储到下标为下标为4的地方,6存储到下标为6的地方。就算存储十位数百位数也是如此操作。

这样,我们就能通过这个映射关系,可以不经过任何比较,一次直接从表中得到要搜索的元素。

二、哈希冲突

但是通过上面的介绍,相信不少童鞋已经发现了,一个下标只能存储一个数据,如果我们有两个数,转换后的下标相同呢?

即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

倘若数据中发生了哈希冲突,我们应该怎么做呢?

引起哈希冲突的一个原因可能是: 哈希函数设计不够合理 。所以我们要先来了解一下哈希函数的涉及原则:
1、哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值
域必须在 0 m-1 之间
2、哈希函数计算出来的地址能均匀分布在整个空间中
3、哈希函数应该比较简单

常见的哈希函数主要是有两种,一种是直接定址法:

取关键字的某个线性函数为散列地址: Hash Key = A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
另外一种是除留余数法,也是我们一开始用的这种:
设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,
按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址
除此之外还有其他许多的方法,一般来说,哈希函数设计的越巧妙,就越能减少哈希冲突。
当然,在精巧的哈希函数,也难免出现哈希冲突,这个时候就需要我们自己去解决了。
解决哈希冲突两种常见的方法是:闭散列和开散列。

1、闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置
呢?
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。 就相当于上厕所时,在还有空位当代情况下,第一个你选择的坑位已经有人了,那你这个时候肯定不会站在门口等着,而是会选择旁边的位置。
二次探测:从发生冲突的位置开始,以一定变化向后依次探测空位,比如第1处,第2处,随后是第4处,第8处......
在决定好寻找下一个空位置的方法后,我们就设定好比例,当空位已经不足一个比例时(比如已经有十分之七的位置被使用),这样哈希冲突产生的概率就会大大提高,因此就需要我们对其进行扩容操作,随后把原本的值依照新的存储空间大小来进行新的定址操作。
在使用闭散列方法时,我们删除一个元素,不能把他置为空节点之类,因为如果此节点后续仍有同义词,就会影响其的查找。
于是我们需要定义三个状态:空,删除,有值。我们查找一个元素时,碰见删除或者有值的话,就继续查找,直到碰见空就停止了。插入时,碰见有值就继续寻找空位,遇见空和删除就进行插入操作。
通过上面的介绍,我们会发现开散列的方法并不如我们所想的那样解决哈希冲突,所谓堵不如疏,并没有从本质上解决哈希冲突,而是一昧的选择寻找其他空位。导致哈希函数的一一对照关系并不明显表现出来。
那有没有什么方法那个解决这个缺点呢?
答案是开散列。

2、开散列

开散列:开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个同中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
这样有个好处,就是一定会按照哈希函数对应的关系来进行分配,哪怕我此时在插入一个14,24,也只会找到下标位4的链表,随后插入到此链表中,不会跑到下标为3,5的链表里。
为了防止一条链表的数据过多,影响性能,我们一般也会对其进行扩容操作,而开散列方法,只需要将链表转移到新哈希表中就行,不必要在全部拷贝一份数据。

三、其他数据类型的存储问题

哈希函数采用处理余数法,被模的key必须要为整形才可以处理,我们之前的思路只能解决int类型的存储问题,如果那个值是string,是char,我们又应该怎么解决呢?

string与char类型不能被取余,我们想到,那就把它转化为int类型不就可以了吗。

由于字符串长度我们不能确定,但abcd与acbd两个字符串的ASCII码值确实一样,如果光是ASCII码值之和来计算,难免会出现比较离谱的存储结果。据此,通过研究,我们可以通过一些条件来减少ASCII码值的巧合:

class Str_to_Int
{
public:
    size_t operator()(const string& s)
   {
        const char* str = s.c_str();
        unsigned int seed = 131; // 31 131 1313 13131 131313
        unsigned int hash = 0;
        while (*str)
       {
            hash = hash * seed + (*str++);
       }
        
        return (hash & 0x7FFFFFFF);
   }
};

通过这种处理,就能明显减少巧合的发生,将其分配到正确的地址上。


四、哈希表闭散列线性探测实现

我们先写一个简单的哈希表的闭散列实现来理解一下哈希表的底层逻辑。

#pragma once
#include<vector>

// 哈希函数采用除留余数法
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

// 哈希表中支持字符串的操作
template<>//这是对前面模板HashFunc的string特化类型
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto e : key)
		{
			hash *= 31;//防止abcd与dcba的ASCII码值之和相同
			hash += e;
		}

		return hash;
	}
};

// 以下采用开放定址法,即线性探测解决冲突
namespace open_address
{
	enum State
	{
		EXIST,
		EMPTY,
		DELETE

	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_tables.resize(10);
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))//如果以前插入过相同键值
			{
				return false;
			}

			if ((_n * 10) / _tables.size() >= 7)//扩容
			{
				HashTable<K, V, Hash>newh;
				newh._tables.resize(2 * _tables.size());

				for (int i = 0; i < _tables.size(); ++i)
				{
					if (_tables[i]._state == EXIST)
					{
						newh.Insert(_tables[i]._kv);
					}
				}
				_tables.swap(newh._tables);
			}

			Hash h;

			size_t index = h(kv.first) % _tables.size();//确定插入下标
			while (_tables[index]._state == EXIST)
			{
				++index;
				index = index % _tables.size();
			}
			_tables[index]._state = EXIST;
			_tables[index]._kv = kv;
			++_n;
			return true;
		}

		HashData<K, V>* Find(const K& key)
		{
			Hash h;
			size_t index = h(key) % _tables.size();//确定查找下标


			while (_tables[index]._state != EMPTY)
			{
				if ( key == _tables[index]._kv.first)
				{
					return &_tables[index];
				}
				++index;
				index %= _tables.size();
			}

			return nullptr;
		}
		bool Erase(const K& key)
		{
			HashData<K, V>* ret = find(key);
			if (ret)
			{
				ret->_state = DELETE;
				return true;
			}
			else
			{
				return false;
			}
		}

	private:
		vector<HashData<K, V>> _tables;
		size_t _n = 0;  // 表中存储数据个数
	};
}

慢慢看这层代码。

我们用K代表key值,V代表Value值,用Hash来代表一个模板函数,这个函数是为了实现我们的转化key值的作用(就是string类型的key转化为int值)。

我们首先实现了哈希函数的模板,让任意类型的K值得以转化为int类型的参数。注意:对于能够转化为int类型的内置类型,我们直接使用强制转化就行,但是对于经常常用到的string,却又不能直接转换为int,我们就可以写一个特化,要求当K为string时直接调用我们的特化函数就行了。

随后在我们的作用于中定义一个枚举类型,代表上面说的三个状态:存在,空,删除。

寻常的内置类型自然不会包含我们才定义的枚举状态,自然就需要定义一个自定义类型。于是HashData出世了。

随后就是平常的接口的编写:

对于find接口,如果我们找到了对应的值,就需要返回这个值的指针HashData<K, V>*,如果没找到,就返回空指针。而查找就是先通过Hash,来找到初始的键值处,开始线性查找直到找到或者为空找不到。

对于insert插入接口,我们先判断是否已经插入过相同键值,然后在判断是否达到扩容标准,如果达到了,就进行扩容操作(创建一个新的哈希数组,随后复用insert进行插入,最后交换两个哈希数组就行,新创建的会自动进行销毁)。扩容后,也是先通过Hash,来找到初始的键值,但我们这次应该通过线性探测来查找空位置或者删除的位置。

对于erase接口,我们可以先复用find找到相应的位置,随后把其的_state属性改为delete就行,不必进行数据内容上的修改。我们访问任意一个地址,都是先判断其state属性是否满足条件。

五、哈希表开散列哈希桶的实现

先看代码:

#pragma once
#include<vector>


template<class K>
struct HashFunc//哈希函数,把K类型转化为int
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
}; 
template<>
struct HashFunc<string>//当K类型时string时的特化函数
{
	size_t operator()(const string& s)
	{
		size_t ret = 0;
		for (auto &it : s)//我们这里对string的每个字母采用乘以31再相加的方法
		{
			ret *= 31;
			ret += it;
		}
		return ret;
	}
};



namespace hash_bucket
{
	template<class T>
	struct HashNode//哈希桶存储的单链表的节点结构
	{
		T _data;
		HashNode<T>* next;
		HashNode(const T&data)
			:_data(data)
			,next(nullptr)
		{}
	};

	template<class K,class T,class Hash=HashFunc<K>>
	class HashTable
	{
		struct keyofT//我这里的实现方法有些特殊,多增加了一个keyofT函数,这个函数时为了后面用哈希桶实现unordered_map与unordered_set
			//而实现的,由于那个时候哈希桶才是底层,所以现在只使用底层代码就会变得奇怪
		{
			const K& operator()(const T&kv)//传递一个string ,int类型的参数就得
				//HashTable<string, pair<string, int>>hash;
			{
				return kv.first;
			}
		};
	public:
		typedef HashNode<T> Node;
		HashTable()
			:_n(0)
		{
			_tables.resize(10);
		}

		~HashTable()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

		bool insert(const T& data)
		{
			Hash h;
			if (_n >= _tables.size())//扩容
			{
				vector<Node*> newtables(_tables.size() * 2, nullptr);
				for (int i = 0; i < _tables.size(); ++i)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->next;
						size_t newindex = h(keyofT()(cur->_data)) % newtables.size();
						cur->next = newtables[newindex];
						newtables[newindex] = cur;
						cur = next;   
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newtables);
			}
			Node* newnode = new Node(data);
			size_t index = h(keyofT()(data)) % _tables.size();
			newnode->next = _tables[index];
			_tables[index] = newnode;
			++_n;

			return true;
		}

		Node* find(const K& key)
		{
			Hash h;
			size_t index = h(key) % _tables.size();
			Node* cur = _tables[index];
			while (cur)
			{
				if (cur->_data.first == key)
				{
					return cur;
				}
				else
				{
					cur = cur->next;
				}
			}

			return nullptr;
		}


		bool erase(const K& key)
		{
			Hash h;
			size_t index = h(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[index];
			while (cur)
			{
				if (cur->_data.first == key)
				{
					if (prev == nullptr)
					{
						_tables[index] = cur->next;
					}
					else
					{
						prev->next = cur->next;
					}

					delete cur;
					cur = nullptr;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->next;
				}
			}
			return false;
		}
	private:
		vector<Node*>_tables;
		size_t _n;
	};
};

相较于闭散列,stl库里实现unordered_map与unordered_set两个容器时底层都用的开散列,所以我这里的开散列实现的有些奇怪,增加的keyofT函数更有利于后续的封装容器。

但是大体结构仍然没有改变,同样用到了Hash来解决不同类型转化为int的问题。唯一值得一提的就是由于我们的节点是指针的链接方式,所以扩容时,我们不需要再赋值节点,只需要把每个节点指针插入到新的哈希table里进行交换就行。

六、哈希表性能分析

哈希表的性能主要取决于哈希函数的设计和哈希冲突的处理方式。哈希表在最理想的情况下,即哈希函数将元素均匀分布到哈希表中时,查找、插入、删除操作的时间复杂度为 O(1)O(1)O(1)。但当发生大量哈希冲突时,时间复杂度可能退化到 O(n)O(n)O(n),这是最坏情况。为了优化性能,我们可以从以下几个方面着手:

  1. 设计良好的哈希函数:哈希函数应尽可能均匀地将元素分布到哈希表中,避免哈希冲突。对数据的特性进行分析,选择合适的哈希函数,如前文提到的直接定址法、除留余数法等。

  2. 扩容:当哈希表中存储的元素个数接近表容量时,哈希冲突的概率会增加,因此需要动态扩容,保持较低的装载因子(如装载因子不超过0.7)。

  3. 合理选择哈希冲突解决策略:开散列(链地址法)通常比闭散列(开放定址法)表现更好,尤其是在高装载因子的情况下,链表法通过链表的结构减少了冲突对性能的影响。

七、哈希表应用场景

哈希表作为一种高效的数据结构,应用非常广泛,特别是在需要快速查找的场景中。例如:

  1. 数据库索引:哈希表在数据库系统中用于索引结构,能够快速查找数据。

  2. 缓存系统:例如Redis等内存缓存系统广泛使用哈希表存储键值对,实现高效的数据存取。

  3. 集合类操作:哈希表在语言标准库中的实现,如C++的unordered_mapunordered_set,用于高效的查找和去重操作。

  4. 字典查找:哈希表是构建字典和符号表的基础,广泛用于自然语言处理、编译器等场景。

八、哈希表的优缺点

优点

  • 查找、插入、删除操作在理想情况下的时间复杂度为 O(1)O(1)O(1),性能非常高效。
  • 实现简单,适合键值对的快速存储和检索。

缺点

  • 在发生大量哈希冲突的情况下,性能可能退化到 O(n)O(n)O(n)。
  • 哈希函数的设计需要谨慎,容易出现偏斜分布,从而影响性能。
  • 哈希表无法保证元素的顺序,适用于无序集合或字典的应用场景。

九、总结

哈希表作为一种重要的数据结构,提供了高效的查找、插入和删除操作。通过设计良好的哈希函数和适当的冲突解决策略,可以最大化哈希表的性能。了解哈希表的工作原理和实现方式,有助于在实际应用中选择合适的解决方案,并有效提升系统的性能。

希望本篇文章对大家有所帮助!

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

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

相关文章

C++(进阶) ─── 继承

目录 1.继承的概念及定义 1.1继承的概念 1.2 继承定义 1.2.1定义格式 1.2.2继承关系和访问限定符 1.2.3继承基类成员访问方式的变化 2.基类和派生类对象赋值转换 3.继承中的作用域 4.派生类的默认成员函数 5.继承与友元 6. 继承与静态成员 7.复杂的菱形继承及菱形虚拟继承 8.继…

ARCGIS PRO DSK MapTool

MapTool用于自定义地图操作工具&#xff0c;使用户能够在ArcGIS Pro中执行特定的地图交互操作。添加 打开MapTool1.vb文件&#xff0c;可以看到系统已经放出MapTool1类&#xff1a; Public Sub New()将 IsSketchTool 设置为 true 以使此属性生效IsSketchTool TrueSketchTyp…

秋招测评为什么有行测题型?有没有训练题库?

为什么有行测题型&#xff0c;那这就得看看行测题型的作用了。 1、行测题可以比较全面评估应聘者的基本素质&#xff0c;包括数学能力、语言能力、逻辑思维能力等。这些能力是从事各类职业所必需的基本能力&#xff0c;对于判断应聘者的学习潜力和工作效率具有重要意义。 2、…

MySQL基于GTID同步模式搭建主从复制

系列文章目录 rpmbuild构建mysql5.7.42版本的rpm包 文章目录 系列文章目录一、mysql-5.7.42RPM包构建二、同步模式分类介绍1.异步同步模式2.半同步模式2.1.实现半同步操作流程2.2.半同步问题总结2.3.半同步一致性2.4.异步与半同步对比 3.GTID同步 三、GTID同步介绍1.gtid介绍2…

如何准备多台虚拟机并配置集群化软件

在搭建集群化软件的过程中&#xff0c;首先需要准备好多台Linux服务器。本文将详细介绍如何使用VMware提供的克隆功能来准备多台虚拟机&#xff0c;并进行必要的配置以实现集群化软件的部署。 1. 准备多台虚拟机 安装集群化软件&#xff0c;首要条件就是要有多台Linux服务器可…

nvm无法下载npm的问题

1、问题 执行 nvm install 14.21.3 命令&#xff0c;node可以正常下载成功&#xff0c;npm下载失败 2、nvm配置信息 …/nvm/settings.txt root: D:\soft\nvm path: D:\soft\nodejs node_mirror: npmmirror.com/mirrors/node/ npm_mirror: registry.npmmirror.com/mirrors/…

Java面试篇基础部分-Java内部类介绍

首先需要了解什么是内部类,内部类就是定义在类的内部的类称为内部类,内部类可以根据不同的定义方式分为静态内部类、成员内部类、局部内部类和匿名内部类。 静态内部类 定义在类体内部的通过static关键字修饰的类,被称为静态内部类。静态内部类可以访问外部类的静态变量和…

BEV学习---LSS-3--(体素坐标系及各种坐标系的理解)

1、体素坐标系 如下两个链接&#xff0c;详细介绍了对体素坐标系的理解&#xff1a; 体素坐标(voxel_coors)在mmdetection3d中的理解_体素坐标系-CSDN博客 3D目标检测中坐标系详解_点云用的什么坐标系-CSDN博客 2、自动驾驶中各种坐标系的定义及相互转换 【KnowledgeBase】…

lamp和nginx的搭建

lamp搭建 下载需要用到的 yum install php yum install php-mysql yum install php-mbstring 进入到html路径下&#xff0c;将文件复制到该路径 解压文件 将文件夹里的内容都复制到html下 在配置文件中添加页面index.php 此时打开网页提示需添加可写权限 但因为直接添加不…

Java实现发送邮件如何配置SMTP和认证信息?

Java实现发送邮件的关键要点&#xff1f;Java怎么实现邮件发送&#xff1f; Java作为一种强大的编程语言&#xff0c;提供了丰富的库和工具来实现邮件发送功能。AokSend将详细介绍如何在Java中配置SMTP服务器和认证信息&#xff0c;以实现邮件发送功能。 Java实现发送邮件&am…

企业级镜像容器的访问控制

为保障镜像制品及企业版实例安全&#xff0c;需要配置公网的访问控制策略&#xff0c;以限制通过公网访问企业版实例。 ps: 本功能只能在企业版实例使用&#xff0c;对于个人版实例不支持使用此功能。 操作步骤 1、登录容器镜像控制台 &#xff1b; 2、在顶部菜单栏&#xf…

鱼类检测-目标检测数据集(包括VOC格式、YOLO格式)

鱼类检测-目标检测数据集&#xff08;包括VOC格式、YOLO格式&#xff09; 数据集&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1B4o8IgOmAWeQJDWpJWxqXg?pwdjaco 提取码&#xff1a;jaco 数据集信息介绍&#xff1a; 共有 2848 张图像和一一对应的标注文件 标注文…

[图论]街道赛跑

题目描述 图一表示一次街道赛跑的跑道。可以看出有一些路口&#xff08;用 0 0 0 到 N N N 的整数标号&#xff09;&#xff0c;和连接这些路口的箭头。路口 0 0 0 是跑道的起点&#xff0c;路口 N N N 是跑道的终点。箭头表示单行道。运动员们可以顺着街道从一个路口移动到…

自测的重要性

1、把debug一遍&#xff0c;看看每一步变量值的变化都符合预期 2、核对需求文档&#xff0c;看看是不是自己的逻辑跟需求都是匹配的&#xff0c;有没有遗漏的细节 3、有时候配合接口的使用方去做点假数据&#xff0c;也是发现自己接口漏洞的好机会 发现了sql少写了个条件、发…

【Go】Go语言中的流程控制语句

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

Python识别拖放的PDF文件再转成文本文件

日常工作中经常用到PDF文件&#xff0c;有些PDF文件的文字是不能复制的&#xff0c;为了复制这些文字&#xff0c;我们需要转化PDF文件&#xff0c;或者采用微信的OCR图片识别文字&#xff0c;这样非常不方便。为此&#xff0c;我编写了一个Python小程序&#xff0c;利用Tkinte…

基于51单片机的多功能台灯Protues仿真设计

目录 一、设计背景 二、设计要求 三、仿真演示 四、程序展示 一、设计背景 随着科技的飞速发展和智能家居的普及&#xff0c;传统的台灯已经难以满足现代消费者对照明设备的多样化需求。传统台灯的功能主要集中在提供基本的照明效果&#xff0c;其操作方式通常是通过手动调…

Langchain.js你必须要知道的核心组件

关于Langchain.js Langchain.js&#xff0c;在github上截止到今日已经有92k的start。之前一直偶有耳闻&#xff0c;但没有深入了解。今天看完后&#xff0c;真的是可以堪称大模型里的瑞士军刀。 LangChain由Harrison Chase于2022年10月作为开源软件项目推出&#xff0c;用于连…

抗菌肽;Parasin I;KGRGKQGGKVRAKAKTRSS;CAS号:219552-69-9

【Parasin I 简介】 Parasin I是一种抗菌肽&#xff0c;由19个氨基酸组成&#xff0c;最初从鲶鱼的皮肤粘液中分离得到。它具有广谱的抗菌活性&#xff0c;能够有效对抗革兰氏阳性菌和革兰氏阴性菌&#xff0c;包括一些对传统抗生素具有耐药性的菌株。 【中文名称】抗菌肽 Par…

C语言 11 字符串

前面学习了数组&#xff0c;而对于字符类型的数组&#xff0c;比较特殊&#xff0c;它实际上可以作为一个字符串&#xff08;String&#xff09;表示&#xff0c;字符串就是一个或多个字符的序列&#xff0c;比如在一开始认识的"Hello World"&#xff0c;像这样的多个…