草庐IT

详解二叉树,带你彻底搞懂二叉树、堆排序 、向上调整算法、向下调整算法【数据结构】

鄃鳕 2023-06-07 原文

文章目录

是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因
为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。

  • 有一个特殊的结点,称为根结点,根节点没有前驱结点
  • 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i<= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
    因此,树是递归定义的

注意:树形结构中,子树之间不能有交集,否则就不是树形结构

树的相关概念

  • 节点的一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
  • 叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点
  • 非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G…等节点为分支节点
  • 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
  • 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
  • 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
  • 树的一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
  • 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
  • 树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
  • 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
  • 节点的祖先从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
  • 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
  • 森林:由m(m>0)棵互不相交的树的集合称为森林

树的表示

孩子兄弟表示法

树结构相对线性表比较复杂,存储起来比较麻烦,树既然保存值域,也要保存结点和结点之间
的关系 ,我们采用常用的孩子兄弟表示法 ,即左孩子右兄弟右兄弟是指亲兄弟 ,不是表(堂)兄弟

A 的第一个孩子是B ,A 只指向B ,B的亲兄弟是C ,B指向第一个孩子节点D ,D指向亲兄弟节点E 、F

typedef int DataType;
struct TreeNode
{
	struct TreeNode* firstChild1;//第一个孩子节点 

	struct TreeNode* pNextBrother;//指向其下一个兄弟节点 
	DataType data;

};

特殊的二叉树

满二叉树

一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是
说,如果一个二叉树的层数为K,根据等比数列求和 算出 (2^k) - 1 ,则它就是满二叉树

完全二叉树

高度为k的完全二叉树 , 前 k -1 都是满的 ,最后一层要求从左到右是连续的, 满二叉树是一种特殊的完全二叉树 ,完全二叉树节点个数最大是满二叉树的状态 ,个数为(2^k)-1

完全二叉树节点个数最小是前k-1 层的总数再加上第K层的第一个节点 ,前k-1 层的总数是2 ^(k-1) -1 ,所以个数是 2 ^ (k-1 )

二叉树性质

对任何一颗二叉树,如果度为0其叶节点数为n0 ,度为2的分支节点个数为n2 ,则有n0 = n2 +1
度为0的永远比度为2的多一个节点
对于一颗完全二叉树n1要么为0 要么是1

二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结
构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统
虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

完全二叉树的值在数组位置中父子下标的关系
parent = (child-1) /2
leftchild = parent *2 +1
rightchild =parent *2 +2

堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。

小根堆

树中所有父亲都小于或等于孩子

大根堆

树中所有父亲都大于或等于孩子

堆的实现

堆的初始化

void HeapInit(Heap* php)
{
	assert(php);
    php->a = (HeapDataType*)malloc(sizeof(HeapDataType) *4);
	if (php->a == NULL)
	{
		printf("malloc fail");
		exit(-1);
	}
	php->capacity = 4;
	php->size = 0;
}

堆向上调整算法(logN)

前提 : 左右子树必须是大堆或者小堆

向堆中插入数据,需要使用向上调整算法,因为向堆中插入数据是将数据插入到下标为size的位置,插入一个数据,size++,此时可能就不满足小堆(或大堆),因此要对其进行调整

向上调整算法
先将元素插入到堆的末尾,即最后一个孩子之后
从插入的结点位置开始和父节点比较
插入之后如果堆的性质遭到破坏,将新插入的节点顺着双亲往上调整到合适的位置即可

void AdjustUp(HeapDataType* a, int child) //向上调整算法
{
	int parent = (child-1)/2; //父子节点下标关系推论
	while (child >0 )
	{
		//大队根
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			//向上调整
			child = parent;
			parent = (child - 1) / 2;//更新parent
		}
		//不满足大堆根条件
		else
		{
			break;
		}
	}
}

堆的插入

插入一个数据是插入到数组的末尾,即树形结构的最后一层的最后一个结点,插入数据仍然需要保持堆的结构,所以插入数据后需要使用堆的向上调整算法对堆进行调整

void HeapPush(Heap* php, HeapDataType x)
{
	assert(php);
	if (php->capacity == php->size)
	{
		//扩容
		HeapDataType*  tmp = (HeapDataType *)realloc(php->a, sizeof(HeapDataType) * php->capacity * 2);
		//扩容失败
		if (tmp == NULL)
		{
			printf("realloc fail");
			exit(-1);
		}

		//扩容成功
		php->a = tmp;
		php->capacity *= 2;

	}
	php->a[php->size] = x;
	php->size++;
	AdjustUp(php->a, php->size-1);
}

以大堆根为例 ,插入一个60


向下调整算法

前提 : 左右子树必须是大堆或者小堆

从根节点处开始,选出左右孩子中值较大的孩子
父节点和较大的子节点进行比较, 如果父节点的数据比大的那个孩子结点的数据要小,那就进行交换

在选左右孩子节点较大的时候,我们可以使用假设逻辑 ,假设默认左孩子大于右孩子, 如果左孩子大于右孩子 ,则假设成立 , 如果左孩子小于右孩子 ,child++ ,指向右孩子节点

最坏的情况下调整到叶子节点为止
那如何判断是否调整到叶子节点? 如果调整到叶子节点,也就意味着没有子节点,换句话说就是子节点超出了数组的范围

这里以大堆为例 ,以5为根节点的左右子树,都满足大堆的性质,只有根节点不满足大堆的性质,因此只需要将根节点向下调整到合适的位置,即可形成堆结构


void AdjustDown(HeapDataType* a, int n, int parent) // parent 是下标 , n 是数组元素个数
{
	int child = parent * 2 + 1; // 父子节点之间的关系
	while (child < n ) //调整到叶子节点结束 ,即超出数组范围
	{ 
		//以大堆为例 , 左右孩子比较 ,选出较大值
		//假设默认左孩子大于右孩子 
		if ( child+1<n && a[child + 1] > a[child])  // 右孩子是否存在 ,防止越界
		{
			child++; //如果右孩子存在,并且右孩子结点的数据大于左孩子结点的数据,就child++,此时child指向右孩子结点,这样永远保证是左右孩子的较大值 
		}
		if (a[child] > a[parent])
		{

			Swap(&a[child], &a[parent]); // 交换
			parent = child;
			child = parent * 2 + 1; //更新子节点
		}
		else
		{
			break; 
		}
	}
}


堆的删除

堆的删除以大堆根为例
如果直接挪动数据 ,时间复杂度为O(N) ,且破化了堆中的父子兄弟关系 ,堆的删除采用向下调整算法

堆的删除其实就是删除堆顶元素(最大的元素)
先将数组末尾的元素与堆顶元素交换,size-- , 堆顶元素就被删除了
删除堆顶数据之后 ,堆的结构就被破坏了 ,使用向下调整算法 ,恢复堆的结构

void HeapPop(Heap* php) // 删除 
{
	assert(php);
	assert(!HeapEmpty(php));
	// 堆顶和数组最后一个元素交换
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--; //删除
	//向下调整算法
	AdjustDown(php->a, php->size , 0);

}

拿到堆顶的数据

获取堆顶的数据,即返回数组下标为0的数据

HeapDataType  HeapTop(Heap* php) // 拿到堆顶的数据
{
	assert(php);
	return php->a[0];
}

获取堆的数据个数

获取堆的数据个数,即返回堆结构体中的size变量

int HeapSize(Heap* php)// 获取堆的数据个数
{
	assert(php);
	return php->size;
}

堆是否为空

堆的判空,即判断堆结构体中的size变量是否为0

bool HeapEmpty(Heap* php)  // 堆是否为空
{
	assert(php);
	
	return php->size == 0; 
}

完整代码
Test.c

#include"Heap.h"

void TestHeap1()
{
	Heap hp;
	HeapInit(&hp);
	HeapPush(&hp , 70);
	HeapPush(&hp, 56);
	HeapPush(&hp, 30);
	HeapPush(&hp, 25);
	HeapPush(&hp, 15);
	HeapPush(&hp, 10);
	HeapPush(&hp, 20);
	HeapPush(&hp, 60);

	//打印
	int k = 0;
	scanf_s("%d",&k);
	while (!HeapEmpty(&hp) && k--)
	{
		printf("%d ", HeapTop(&hp));
		HeapPop(&hp);
	}
	printf("\n");



}
 void TestHeap2()
{
	 Heap hp;
	 HeapInit(&hp);
	 HeapPush(&hp, 70);
	 HeapPush(&hp, 56);
	 HeapPush(&hp, 30);
	 HeapPush(&hp, 25);
	 HeapPush(&hp, 15);
	 HeapPush(&hp, 10);
	 HeapPush(&hp, 20);
	 HeapPush(&hp, 60);

	 HeapPop(&hp);
	 HeapPop(&hp);
	 HeapPop(&hp);

}
 void TestHeap3()
 {
	 Heap hp;
	 HeapInit(&hp);
	 HeapPush(&hp, 70);
	 HeapPush(&hp, 56);
	 HeapPush(&hp, 30);
	 HeapPush(&hp, 25);
	 HeapPush(&hp, 15);
	 HeapPush(&hp, 10);
	 HeapPush(&hp, 20);
	 HeapPush(&hp, 60);

int a =  HeapTop(&hp);
	int c =  HeapSize(&hp);
	 

  }
 void TestHeap4()
 {
	 Heap hp;
	 HeapInit(&hp); 
	 HeapEmpty(&hp);
  }

//int main()
//{
//	TestHeap1();
//	//TestHeap2();
//	/*TestHeap3();*/
//	//TestHeap4();
//	return 0;
//}

Heap.c

#include"Heap.h"
void HeapInit(Heap* php)
{
	assert(php);
    php->a = (HeapDataType*)malloc(sizeof(HeapDataType) *4);
	if (php->a == NULL)
	{
		printf("malloc fail");
		exit(-1);
	}
	php->capacity = 4;
	php->size = 0;
}
void Swap(HeapDataType* p1, HeapDataType* p2)
{
	HeapDataType  x = *p1;
	*p1 = *p2;
	*p2 = x;
}
void AdjustUp(HeapDataType* a, int child) //向上调整算法 ,child 是插入数据下标
{
	int  parent = (child-1)/2; //父子节点下标关系推论
	while (child >0 )
	{
		//大堆根
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			//向上调整
			child = parent;
			parent = (child - 1) / 2;//更新parent
		}
		//不满足大堆根条件
		else
		{
			break;
		}
	}
}


void HeapPush(Heap* php, HeapDataType x)
{
	assert(php);
	if (php->capacity == php->size)
	{
		//扩容
		HeapDataType*  tmp = (HeapDataType *)realloc(php->a, sizeof(HeapDataType) * php->capacity * 2);
		//扩容失败
		if (tmp == NULL)
		{
			printf("realloc fail");
			exit(-1);
		}

		//扩容成功
		php->a = tmp;
		php->capacity *= 2;

	}
	php->a[php->size] = x;
	php->size++;
	AdjustUp(php->a, php->size-1);  // php->size-1 是插入数据的下标
}




void AdjustDown(HeapDataType* a, int n, int parent) // parent 是下标 , n 是数组元素个数
{
	int child = parent * 2 + 1; // 父子节点之间的关系
	while (child < n ) //调整到叶子节点结束 ,即超出数组范围
	{ 
		//以大堆为例 , 左右孩子比较 ,选出较大值
		//假设默认左孩子大于右孩子
		if ( child+1<n && a[child + 1] > a[child])  // 右孩子是否存在 ,防止越界
		{
			child++; 
		}
		if (a[child] > a[parent])
		{

			Swap(&a[child], &a[parent]); // 交换
			parent = child;
			child = parent * 2 + 1; //更新子节点
		}
		else
		{
			break; 
		}
		
	}


}
void HeapPop(Heap* php) // 删除 
{
	assert(php);
	assert(!HeapEmpty(php));
	// 堆顶和数组最后一个元素交换
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--; //删除
	//向下调整算法
	AdjustDown(php->a, php->size , 0);

}

HeapDataType  HeapTop(Heap* php) // 拿到堆顶的数据
{

	assert(php);
	return php->a[0];
}
int HeapSize(Heap* php)// 获取堆的数据个数
{
	assert(php);
	return php->size;
}
bool HeapEmpty(Heap* php)  // 堆是否为空
{
	assert(php);
	
	/*return php->size == 0; */
	if (php->size == 0)
	{
		return true;
	}
	else
	{
		return false;
	}
}

Heap.h

#include<stdio.h>
#include<stdbool.h>
#include<stdlib.h>
#include<assert.h>	
typedef int HeapDataType;
typedef struct Heap
{
	HeapDataType * a;
	int capacity;//存储容量 
	int size; //  实际大小
} Heap;

void HeapInit(Heap * php);

void Swap(HeapDataType* p1, HeapDataType* p2);

void AdjustUp(HeapDataType* a, int child); //向上调整算法

void HeapPush(Heap* php, HeapDataType x);

void AdjustDown(HeapDataType* a, int child, int n); 

void HeapPop(Heap* php); // 删除 

HeapDataType  HeapTop(Heap* php);  // 拿到堆顶的数据

int HeapSize(Heap* php); // 获取堆的数据个数

bool HeapEmpty(Heap* php);  // 堆是否为空

堆排序

排降序 建立小堆
排升序 建立大堆

升序

向上调整建堆,时间复杂度为O(N* longN)

使用向上调整算法建大堆,将数组建成大堆后,此时堆顶元素是最大的 ,将堆顶元素和最后一个元素进行交换,这样最大的元素就到了数组最后一个元素,对剩下的元素使用向下调整 , 当下一次向下调整时,我们不管这个处在数组最后一个位置的最大元素(有点类似堆的删除 ),此时第二大的元素来到的堆顶 ,堆顶元素继续与最后一个元素进行交换,(注意第一个交换过去的最大的元素已经不在范围内了) ,依次类推 ,升序就完成了

 void HeapSort(int* a, int n)
 {
	 //向上调整建堆
	 for (int i = 0; i < n; i++)
	 {
		 AdjustUp(a, i);
	}
	 //向下调整排序
	 int end = n - 1;// end 是最后一个元素的下标
	 while (end > 0)
	 {
		 Swap(&a[0], &a[end]);
		 AdjustDown(a, end, 0);
		 end--;
	 }
 }
 int main()
 {
	 int a[10] = { 2, 1, 5, 7, 6, 8, 0, 9, 4, 3 };
	 HeapSort(a, 10);
	 return 0; 
 }

向下调整建堆的前提是左右子树都是堆 ,从倒数第一个非叶子节点开始倒着调整,如何找到倒数第一个非叶子节点?通过最后一个节点的父节点来找到 , 那为什么要找倒数第一个非叶子节点? 因为倒数第一个非叶子节点的左右子树都满足大堆或小堆的条件


 void HeapSort(int* a, int n)
 {
	 //向下调整建堆
	 for (int i = (n-1-1)/ 2; i >= 0; i--) // n-1是最后一个节点的下标,(n-1-1)/2 通过下标找到最后一个节点的父节点
	 {
		 AdjustDown(a,n , i);
	 }
	 //向下调整排序
	 int end = n - 1; //end 是最后一个元素的下标 
	 while (end >=0)
	 {
		 Swap(&a[0], &a[end]);
		 AdjustDown(a, end, 0);
		 end--;
	 }

 }
 int main()
 {
	 int a[10] = { 2, 1, 5, 7, 6, 8, 0, 9, 4, 3 };
	 HeapSort(a, 10);
	 return 0; 
 }

建堆时间复杂度

向上调整建堆——O(N*logN)

向下调整建堆—— O(N)

TOP-K问题

即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大

找N个数中最大的前k个一般建立N个的大堆 ,再Pop K 次就完成了,这种思路适合数据量比较小

如果数据量比较大
前K个数据建一个小堆
遍历剩下的元素,如果这个数据比堆顶的数据大,就将这个数据代替堆顶数据进堆( 向下调整)
最后小堆的数据就是最大的前K个

用数据集合中前K个元素来建堆

  • 前k个最大的元素,则建小堆
  • 前k个最小的元素,则建大堆

如果你觉得这篇文章对你有帮助,不妨动动手指给点赞收藏加转发,给鄃鳕一个大大的关注
你们的每一次支持都将转化为我前进的动力!!!

有关详解二叉树,带你彻底搞懂二叉树、堆排序 、向上调整算法、向下调整算法【数据结构】的更多相关文章

  1. 区块链之加解密算法&数字证书 - 2

    目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非

  2. ruby - 在 Ruby 中实现二叉树 - 2

    我一直在尝试在Ruby中实现BinaryTree类,但我得到了stackleveltoodeep错误,尽管我似乎没有在该特定代码段中使用任何递归:1.classBinaryTree2.includeEnumerable3.4.attr_accessor:value5.6.definitialize(value=nil)7.@value=value8.@left=BinaryTree.new#stackleveltoodeephere9.@right=BinaryTree.new#andhere10.end11.12.defempty?13.(self.value==nil)?true:

  3. 100个python算法超详细讲解:画直线 - 2

    1.问题描述使用Python的turtle(海龟绘图)模块提供的函数绘制直线。2.问题分析一幅复杂的图形通常都可以由点、直线、三角形、矩形、平行四边形、圆、椭圆和圆弧等基本图形组成。其中的三角形、矩形、平行四边形又可以由直线组成,而直线又是由两个点确定的。我们使用Python的turtle模块所提供的函数来绘制直线。在使用之前我们先介绍一下turtle模块的相关知识点。turtle模块提供面向对象和面向过程两种形式的海龟绘图基本组件。面向对象的接口类如下:1)TurtleScreen类:定义图形窗口作为绘图海龟的运动场。它的构造器需要一个tkinter.Canvas或ScrolledCanva

  4. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  5. Tcl脚本入门笔记详解(一) - 2

    TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是

  6. ruby - 在 Ruby 中实现 Luhn 算法 - 2

    我一直在尝试用Ruby实现Luhn算法。我一直在执行以下步骤:该公式根据其包含的校验位验证数字,该校验位通常附加到部分帐号以生成完整帐号。此帐号必须通过以下测试:从最右边的校验位开始向左移动,每第二个数字的值加倍。将乘积的数字(例如,10=1+0=1、14=1+4=5)与原始数字的未加倍数字相加。如果总模10等于0(如果总和以零结尾),则根据Luhn公式该数字有效;否则无效。http://en.wikipedia.org/wiki/Luhn_algorithm这是我想出的:defvalidCreditCard(cardNumber)sum=0nums=cardNumber.to_s.s

  7. Ruby 斐波那契算法 - 2

    下面是我写的一个计算斐波那契数列中的值的方法:deffib(n)ifn==0return0endifn==1return1endifn>=2returnfib(n-1)+(fib(n-2))endend它工作到n=14,但在那之后我收到一条消息说程序响应时间太长(我正在使用repl.it)。有人知道为什么会这样吗? 最佳答案 Naivefibonacci进行了大量的重复计算-在fib(14)fib(4)中计算了很多次。您可以将内存添加到您的算法中以使其更快:deffib(n,memo={})ifn==0||n==1returnnen

  8. ruby-on-rails - Rails add_index 算法 : :concurrently still causes database lock up during migration - 2

    为了防止在迁移到生产站点期间出现数据库事务错误,我们遵循了https://github.com/LendingHome/zero_downtime_migrations中列出的建议。(具体由https://robots.thoughtbot.com/how-to-create-postgres-indexes-concurrently-in概述),但在特别大的表上创建索引期间,即使是索引创建的“并发”方法也会锁定表并导致该表上的任何ActiveRecord创建或更新导致各自的事务失败有PG::InFailedSqlTransaction异常。下面是我们运行Rails4.2(使用Acti

  9. ruby - 趋势算法 - 2

    我正在开发一个类似微论坛的项目,其中一个特殊用户发布一条快速(接近推文大小)的主题消息,订阅者可以用他们自己的类似大小的消息来响应。直截了当,没有任何形式的“挖掘”或投票,只是每个主题消息的响应按时间顺序排列。但预计会有很高的流量。我们想根据它们引起的响应嗡嗡声来标记主题消息,使用0到10的等级。在谷歌上搜索了一段时间的趋势算法和开源社区应用示例,到目前为止已经收集到两个有趣的引用资料,但我还没有完全理解它们:Understandingalgorithmsformeasuringtrends,关于使用基线趋势算法比较维基百科页面浏览量的讨论,在SO上。TheBritneySpearsP

  10. Ruby - 不支持的密码算法 (AES-256-GCM) - 2

    我收到错误:unsupportedcipheralgorithm(AES-256-GCM)(RuntimeError)但我似乎具备所有要求:ruby版本:$ruby--versionruby2.1.2p95OpenSSL会列出gcm:$opensslenc-help2>&1|grepgcm-aes-128-ecb-aes-128-gcm-aes-128-ofb-aes-192-ecb-aes-192-gcm-aes-192-ofb-aes-256-ecb-aes-256-gcm-aes-256-ofbRuby解释器:$irb2.1.2:001>require'openssl';puts

随机推荐