1. 前言

程序中所涉及到的任何数据,计算机底层均需转换成二进制数值后方可存储,这个过程也称为编码。反之,把底层二进制数据转换成应用数据称为解码,

不同的数据类型需要不同的编(解)码方案,如音频数据编码、视频数据编码、图形数据编码……

即使是同类型的数据,根据不同的应用场景,也有多种编码方案可选。如字符编译就有ASCII、UTF-8、哈夫曼编码以及本文将要讲解的格雷码

讲解格雷码之前,首先了解一下格雷码的定义:

  • 对数据编码后,若任意两个相邻的码值间只有一位二进制数不同,则称这种编码为格雷码(Gray Code)
  • 由于最大数与最小数之间也仅只有一位数不同,即首尾相连,又称循环码反射码

格雷码的优点:

一种编码的出现,一定是为了弥补已有编码的不足,这里以ASCII编码中的A~Z字符的码值开始研究:

二进制 十进制 十六进制 字符
01000001 65 41 A
01000010 66 42 B
01000011 67 43 C
01000100 68 44 D
01000101 69 45 E
01000110 70 46 F
01000111 71 47 G
01001000 72 48 H
01001001 73 49 I
01001010 74 4A J
01001011 75 4B K
01001100 76 4C L
01001110 78 4E N
01001111 79 4F O
01010000 80 50 P

G的编码是01000111H的编码是01001000

从宏观的计数角度而言,计数增长仅为1,但是有 4 个数据位发生了变化。从底层的存储硬件而言,每一位都需由电路控制 ,宏观世界里4 位数字的变化会引起微观世界里多个电路门变化,且不可能同时发生。

意味着中间会短暂出现其它代码,则在电流不稳或特定因素的影响下可能会导致电路状态变化错误的概率会增加很多。

而格雷码相邻编码只有一个数据位的变化,相对于计数编码,显然易见,其安全性和容错性要高很多。

格雷码可以有多种编码形式。如下图所示:

十进制数 4位自然二进制码 4位典型格雷码 十进制余三格雷码 十进制空六格雷码 十进制跳六格雷码 步进码
0 0000 0000 0010 0000 0000 00000
1 0001 0001 0110 0001 0001 00001
2 0010 0011 0111 0011 0011 00011
3 0011 0010 0101 0010 0010 00111
4 0100 0110 0100 0110 0110 01111
5 0101 0111 1100 1110 0111 11111
6 0110 0101 1101 1010 0101 11110
7 0111 0100 1111 1011 0100 11100
8 1000 1100 1110 1001 1100 11000
9 1001 1101 1010 1000 1000 10000
10 1010 1111 ---- ---- ---- ----
11 1011 1110 ---- ---- ---- ----
12 1100 1010 ---- ---- ---- ----
13 1101 1011 ---- ---- ---- ----
14 1110 1001 ---- ---- ---- ----
15 1111 1000 ---- ---- ---- ----

表中典型格雷码具有代表性,一般说格雷码就是指典型格雷码,它可从自然二进制码转换而来。

Tips: 格雷码是一种变权码,每一位码没有固定的大小,很难直接进行比较大小和算术运算。

2. 编码方案

2.1 递归实现

这种方法基于格雷码是反射码的事实,可以对直接使用递归算法构造。

流程如下:

  • 1位格雷码有两个编码。

  • (n+1)位格雷码中的前2^n个编码等于n位正序格雷码的前面 加0

  • (n+1)位格雷码中的后2^n个编码等于n位逆序格雷码的前面加1

2位格雷码 3位格雷码 4位格雷码 4位自然二进制码
00 000 0000 0000
01 001 0001 0001
11 011 0011 0010
10 010 0010 0011
110 0110 0100
111 0111 0101
101 0101 0110
100 0100 0111
1100 1000
1101 1001
1111 1010
1110 1011
1010 1100
1011 1101
1001 1110
1000 1111

编码实现:

#include <iostream>
#include <vector>
using namespace std;
/*
*实现格雷编码
*/
vector<string> grayCode(int num) {//存储格雷码vector<string> vec;if(num==1) {//出口:1位格雷码是已知的vec.push_back("0");vec.push_back("1");return vec;}//得到低位格雷码vector<string> vec_= grayCode(num-1);//对低位格雷码正向遍历,添加前缀 0vector<string>::iterator begin=vec_.begin();vector<string>::iterator end=vec_.end();for(; begin!=end; begin++) {vec.push_back("0"+*begin);}//对低位格雷码反向遍历,添加前缀 1 vector<string>::reverse_iterator rbegin=vec_.rbegin();vector<string>::reverse_iterator rend=vec_.rend();for(; rbegin!=rend; rbegin++) {vec.push_back("1"+*rbegin);}return vec;
}
//测试
int main(int argc, char** argv) {vector<string> vec=grayCode(4);cout<<"4 位格雷码:"<<endl; for(int i=0; i<vec.size(); i++) {cout<<vec[i]<<endl;}return 0;
}

输出结果:

4 位格雷码:
0000
0001
0011
0010
0110
0111
0101
0100
1100
1101
1111
1110
1010
1011
1001
1000

2.2异或转换

异或转换可以直接把n位二进制数字编码成对应的n位格雷码。当然,也可以把格雷码直接转换成对应的二进制。

编码流程如下:

  • n位二进制的数字,从右到左,以0n-1编号。

  • 如果二进制码字的第i位和i+1位相同,则对应的格雷码的第i位为0(异或操作),否则为1(当i+1=n时,二进制码字的第n位被认为是0,即第n-1位不变)。如下图,二进制 0101经过转换后的格雷码为0111

编码表示

#include <iostream>
#include <vector>
using namespace std;
/*
*异或转换格雷编码
*/
void  yhGrayCode(int num) {//二进制vector<int> vec;//格雷码vector<int>  gc;//存储进位值int jinWei=0;//初始二进制的每一位为 0for(int i=0; i<num; i++) {vec.push_back(0);}//循序递增二进制,并且得到对应的格雷码while (jinWei!=1) {jinWei=0;gc.clear();//第一位不做异或操作gc.push_back(vec[0]);//反序遍历,求相邻两个数字的异或结果int i=0;for(; i<num-1; i++) {if(vec[i]==vec[i+1]) {gc.push_back(0);} else {gc.push_back(1);}}//输出格雷码for( i=0; i<gc.size(); i++) {cout<<gc[i]<<"";}cout<<endl;//更新二进制,递增 1 ,遇到 2 进位jinWei= (vec[num-1]+1) /  2;vec[num-1]=(vec[num-1]+1) % 2;for( i=num-2; i>=0; i--) {vec[i] = vec[i]+jinWei;jinWei= vec[i] / 2;vec[i]=vec[i] % 2;}}
}
//仅测试 4 位格雷码
int main(int argc, char** argv) {cout<<"\ 4 位格雷码:"<<endl;yhGrayCode(4);return 0;
}

输出结果:

解码流程: 解码指把格雷码转换成二进制码。解码的基本思想基于异或运算的加密、解密特性,如果 AB异或得到C。则CB 异或得到ACA 异或得到B

  • 格雷码最左边一位保持不变。

  • 从左边第二位起,将每位与左边解码后的值异或,结果作为该位解码后的值。

  • 依次异或,直到最低位。依次异或转换后的值就是格雷码转换后的自然二进制。

**编码实现:**如下代码仅是对解码的基础逻辑的实现。不具有通用性,可以重构此逻辑,让其更有普遍性。

#include <iostream>
#include <stack>
using namespace std;
/*
* 4 位格雷码转二进制
*/
int main(int argc, char** argv) {//格雷码int grayCode[4]= {0,1,1,1};//二进制int binary[4]= {0};//格雷码最左一位自动解码binary[0]=grayCode[0];//格雷码从左边第二位开始解码for(int i=1; i<4; i++) {if( grayCode[i]==binary[i-1] ) {binary[i]=0;} else {binary[i]=1;}}//输出二进制for(int i=0; i<4; i++) {cout<<binary[i];}return 0;
}

2.3 卡诺图实现

什么是卡诺图?

卡诺图是逻辑函数的一种图形表示。卡诺图是一种平面方格图,每个小方格代表逻辑函数的一个最小项,故又称为最小项方格图。方格图中相邻两个方格的两组变量取值相比,只有一个变量的取值发生变化,按照这一原则得出的方格图(全部方格构成正方形或长方形)就称为卡诺方格图,简称卡诺图。

利用卡诺图生成格雷码的流程如下:

  • 使用卡诺图编码格雷码,总是由低阶生成高阶。可以绘制如下的表格,行号和列号均以低阶格雷码作为标题。

  • 从卡诺图的左上角以字形到右上角最后到左下角遍历卡诺图,依次经过格子的变量取值即为典型格雷码的顺序。

编码实现: 如上图所示,4 位格雷码可由 3 位和 1 位、也可以由 2 位和 2 位的格雷码构建成的卡诺图生成,为了让构建过程具有通用性,基础逻辑:n 位的格雷码都可以能过n-1阶和 1阶格雷码构建的卡诺图生成。如此便可以使用递归算法。

#include <iostream>
#include <cmath>
using namespace std;
/*
* 卡诺图编码
* num 表示二进制位数
*/
string* krtGrayCode(int num) {string* gc;//一阶格雷码if(num==1) {gc=new string[2] {"0","1"};return gc;}//格雷码个数与二进制位数关系int res=pow(2,num);//存储格雷码的数组gc=new string[res];//得到低阶格雷码string* dgc= krtGrayCode(num-1);//一阶格雷码string* oneGc=krtGrayCode(1);//奇偶标志int idx=1;//低阶格雷码的个数int count=pow(2,num-1);int gjIdx=0;//以行优先扫描低阶格雷码for(int i=0; i<count; i++) {if(idx % 2==1) {//奇数行,从左向右和 1 阶格雷码合并for(int j=0; j<=1; j++) {gc[gjIdx++]=dgc[i]+oneGc[j];}} else {//偶数行,从右向左和 1 阶格雷码合并合并for(int j=1; j>=0; j--) {gc[gjIdx++]=dgc[i]+oneGc[j];}}idx++;}return gc;
}
//测试
int main(int argc, char** argv) {int num=4;int count=pow(2,num);string* gc= krtGrayCode(num);for(int i=0; i<count ; i++) {cout<<gc[i]<<endl;}return 0;
}

输出结果:

0000
0001
0011
0010
0110
0111
0101
0100
1100
1101
1111
1110
1010
1011
1001
1000

3. 总结

本文讲解了格雷码的概念以及具体的编码实现方案。

C++ 数学与算法系列之认识格雷码相关推荐

  1. C++数学与算法系列之初等数论

    1. 数 什么是数? 一个用作计数.标记或用作量度的抽象概念. 代表数的一系列符号,包括数字.运算符号等统称为记数系统. 在日常生活中,数通常出现在标记(如公路.电话和门牌号码).序列号和编码上.在数 ...

  2. C++ 数学与算法系列之高斯消元法求解线性方程组

    1. 前言 什么是消元法? 消元法是指将多个方程式组成的方程组中的若干个变量通过有限次地变换,消去方程式中的变量,通过简化方程式,从而获取结果的一种解题方法. 消元法主要有代入消元法.加减消元法.整体 ...

  3. 【CSP 2019】格雷码

    输入文件:2019code.in 输出文件:2019code.out 时间限制:1 s 内存限制:256 MB [题目描述] 通常,人们习惯将所有 n 位二进制串按照字典序排列,例如所有 2 位二进制 ...

  4. 数学之美系列六 -- 图论和网络爬虫 (Web Crawlers)

    数学之美系列六 -- 图论和网络爬虫 (Web Crawlers) [离散数学是当代数学的一个重要分支,也是计算机科学的数学基础.它包括数理逻辑.集合论.图论和近世代数四个分支.数理逻辑基于布尔运算, ...

  5. 大数据算法系列——布隆过滤器

    大数据算法系列--布隆过滤器 一.简介 Bloom filter介绍 Bloom Filter(BF)是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集 ...

  6. 【数学与算法】牛顿法 及其一系列改进算法

    本文转载自牛顿法,在有些概念不清晰的地方,我添加了一些自己的说明. 牛顿法及其变种方法:目录链接: (1) 牛顿法 (2) 拟牛顿条件 (3) DFP 算法 (4) BFGS 算法 (5) L-BFG ...

  7. 排序算法系列:Shell 排序算法

    概述 希尔排序(Shell Sort)是 D.L.Shell 于 1959 年提出来的一种排序算法,在这之前排序算法的时间复杂度基本都是 O(n2n^{2}n2) 的,希尔排序算法是突破这个时间复杂度 ...

  8. 数学之美 系列十六 (下)- 不要把所有的鸡蛋放在一个篮子里 最大熵模型

    数学之美 系列十六 (下)- 不要把所有的鸡蛋放在一个篮子里 最大熵模型 我们上次谈到用最大熵模型可以将各种信息综合在一起.我们留下一个问题没有回答,就是如何构造最大熵模型.我们已经所有的最大熵模型都 ...

  9. 数学之美 系列十三 信息指纹及其应用

    数学之美 系列十三 信息指纹及其应用 任何一段信息文字,都可以对应一个不太长的随机数,作为区别它和其它信息的指纹(Fingerprint).只要算法设计的好,任何两段信息的指纹都很难重复,就如同人类的 ...

最新文章

  1. Java项目:车租赁管理系统(java+Gui+文档)
  2. 写markdown用于Github上readme.md文件
  3. Safari 10默认禁用Flash插件
  4. 手风琴html例子,jquery实现简单手风琴菜单效果实例
  5. 用空代理实现account(lua程序设计21.7练习21.4题)
  6. ad域管理与维护_在NAS SMB卷上使用VisualSVN Server维护代码库
  7. 快速使用nexus搭建maven本地私服
  8. zblock 结构_偷窥Data block 的物理结构
  9. [Python] L1-031. 到底是不是太胖了-PAT团体程序设计天梯赛GPLT
  10. 发送消息的时候,会指定用户,其实还可以定义媒介为脚本,让用户执行这个脚本...
  11. Docker 安装常用软件记录
  12. (一)伤不起--java调用dll
  13. 易语言-万挂作坊4.X下载,有图有真相
  14. JWT原理 对比 appid secretkey 鉴权
  15. 三人行SEO教程免费公开下载-学点啥收集整理
  16. 今天开始使用oschina
  17. 昭阳区计算机学校,昭阳区高级职业中学
  18. 计算机拆机步骤图解,华硕x50拆机步骤图解【图文】
  19. 正则表达式30分钟入门
  20. 840D sl(QT)使用ocx外部控件

热门文章

  1. 使用bedtools进行gwas基因注释
  2. asp.net html silverlight 传参数,Silverlight和ASP.NET相互传参的两种常用方式(QueryString,Cookie)...
  3. CRPR能补偿crosstalk吗?
  4. MAC 打开所有来源的程序
  5. python打印菱形三种方法_用python打印菱形的实操方法和代码
  6. 北大AI公开课13讲全链接+最强干货盘点:视频+笔记+文字实录
  7. 前台js MD5加密 后台 java MD5解密
  8. 关于JavaWeb的分页查询的实现
  9. apmserv php升级方法,APMServ5.2.6 升级php5.2 到 5.3版本,及Memcache升级
  10. 计算机输入法不能切换用户登录,输入法不能正常切换,输入法切换不出来 - 输入法切换不了,输入法不能切换怎么办? - 安全专题...