• 哈希表


    哈希表

    散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

    目录

    基本概念

    常用的构造散列函数的方法

    处理冲突的方法

    查找的性能分析

    实际应用

    字符串哈希函数

    基本概念

      * 若结构中存在关键字和K相等的记录,则必定在f(K)的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系f为散列函数(Hash function),按这个思想建立的表为散列表

      * 对不同的关键字可能得到同一散列地址,即key1≠key2,而f(key1)=f(key2),这种现象称冲突。具有相同函数值的关键字对该散列函数来说称做同义词。综上所述,根据散列函数H(key)和处理冲突的方法将一组关键字映象到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“象” 作为记录在表中的存储位置,这种表便称为散列表,这一映象过程称为散列造表或散列,所得的存储位置称散列地址。这个现象也叫散列桶,在散列桶,只能通过顺序的方式来查找,一般只需要查找三次就可以找到。科学家计算过,当重载因子不超过75%,查找效率最高。

      * 若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function),这就是使关键字经过散列函数得到一个“随机的地址”,从而减少冲突。

    常用的构造散列函数的方法

      散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。

      实际工作中需视不同的情况采用不同的哈希函数,通常考虑的因素有:

      · 计算哈希函数所需时间

      · 关键字的长度

      · 哈希表的大小

      · 关键字的分布情况

      · 记录的查找频率

      1. 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a·key + b,其中a和b为常数(这种散列函数叫做自身函数)。若其中H(key)中已经有值了,就往下一个找,直到H(key)中没有值了,就放进去。

      2. 数字分析法:分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。

      3. 平方取中法:取关键字平方后的中间几位作为散列地址。

      4. 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。数位叠加可以有移位叠加和间界叠加两种方法。移位叠加是将分割后的每一部分的最低位对齐,然后相加;间界叠加是从一端向另一端沿分割界来回折叠,然后对齐相加。

      5. 随机数法:选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。

      6. 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p,p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。

    处理冲突的方法

      1. 开放寻址法:Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:

      1.1. di=1,2,3,…,m-1,称线性探测再散列;

      1.2. di=1^2,-1^2,2^2,-2^2,⑶^2,…,±(k)^2,(k<=m/2)称二次探测再散列;

      1.3. di=伪随机数序列,称伪随机探测再散列。

      2. 再散列法:Hi=RHi(key),i=1,2,…,k RHi均是不同的散列函数,即在同义词产生地址冲突时计算另一个散列函数地址,直到冲突不再发生,这种方法不易产生“聚集”,但增加了计算时间。

      3. 链地址法(拉链法)

      4. 建立一个公共溢出区

    查找的性能分析

      散列表的查找过程基本上和造表过程相同。一些关键码可通过散列函数转换的地址直接找到,另一些关键码在散列函数得到的地址上产生了冲突,需要按处理冲突的方法进行查找。在介绍的三种处理冲突的方法中,产生冲突后的查找仍然是给定值与关键码进行比较的过程。所以,对散列表查找效率的量度,依然用平均查找长度来衡量。

      查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:

      1. 散列函数是否均匀;

      2. 处理冲突的方法;

      3. 散列表的装填因子。

      散列表的装填因子定义为:α= 填入表中的元素个数 / 散列表的长度

      α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的元素较少,产生冲突的可能性就越小。

      实际上,散列表的平均查找长度是装填因子α的函数,只是不同处理冲突的方法有不同的函数。

      了解了hash基本定义,就不能不提到一些著名的hash算法,MD5 和 SHA-1 可以说是目前应用最广泛的Hash算法,而它们都是以 MD4 为基础设计的。那么他们都是什么意思呢?

      这里简单说一下:

      ⑴ MD4

      MD4(RFC 1320)是 MIT 的 Ronald L. Rivest 在 1990 年设计的,MD 是 Message Digest 的缩写。它适用在32位字长的处理器上用高速软件实现--它是基于 32 位操作数的位操作来实现的。

      ⑵ MD5

      MD5(RFC 1321)是 Rivest 于1991年对MD4的改进版本。它对输入仍以512位分组,其输出是4个32位字的级联,与 MD4 相同。MD5比MD4来得复杂,并且速度较之要慢一点,但更安全,在抗分析和抗差分方面表现更好

      ⑶ SHA-1 及其他

      SHA1是由NIST NSA设计为同DSA一起使用的,它对长度小于264的输入,产生长度为160bit的散列值,因此抗穷举(brute-force)性更好。SHA-1 设计时基于和MD4相同原理,并且模仿了该算法。

      那么这些Hash算法到底有什么用呢?

      Hash算法在信息安全方面的应用主要体现在以下的3个方面:

      文件校验

      我们比较熟悉的校验算法有奇偶校验和CRC校验,这2种校验并没有抗数据篡改的能力,它们一定程度上能检测并纠正数据传输中的信道误码,但却不能防止对数据的恶意破坏。

      MD5 Hash算法的"数字指纹"特性,使它成为目前应用最广泛的一种文件完整性校验和(Checksum)算法,不少Unix系统有提供计算md5 checksum的命令。

      数字签名

      Hash 算法也是现代密码体系中的一个重要组成部分。由于非对称算法的运算速度较慢,所以在数字签名协议中,单向散列函数扮演了一个重要的角色。对 Hash 值,又称"数字摘要"进行数字签名,在统计上可以认为与对文件本身进行数字签名是等效的。而且这样的协议还有其他的优点。

      鉴权协议

      如下的鉴权协议又被称作挑战--认证模式:在传输信道是可被侦听,但不可被篡改的情况下,这是一种简单而安全的方法。

      MD5、SHA1的破解

      2004年8月17日,在美国加州圣芭芭拉召开的国际密码大会上,山东大学王小云教授在国际会议上首次宣布了她及她的研究小组近年来的研究成果——对MD5、HAVAL-128、MD4和RIPEMD等四个著名密码算法的破译结果。次年二月宣布破解SHA-1密码。

    实际应用

      以上就是一些关于hash以及其相关的一些基本预备知识。那么在emule里面他具体起到什么作用呢?

      大家都知道emule是基于P2P (Peer-to-peer的缩写,指的是点对点的意思的软件), 它采用了"多源文件传输协议”(MFTP,the Multisource FileTransfer Protocol)。在协议中,定义了一系列传输、压缩和打包还有积分的标准,emule 对于每个文件都有md5-hash的算法设置,这使得该文件独一无二,并且在整个网络上都可以追踪得到。

      什么是文件的hash值呢?

      MD5-Hash-文件的数字文摘通过Hash函数计算得到。不管文件长度如何,它的Hash函数计算结果是一个固定长度的数字。与加密算法不同,这一个Hash算法是一个不可逆的单向函数。采用安全性高的Hash算法,如MD5、SHA时,两个不同的文件几乎不可能得到相同的Hash结果。因此,一旦文件被修改,就可检测出来。

      当我们的文件放到emule里面进行共享发布的时候,emule会根据hash算法自动生成这个文件的hash值,他就是这个文件唯一的身份标志,它包含了这个文件的基本信息,然后把它提交到所连接的服务器。当有他人想对这个文件提出下载请求的时候, 这个hash值可以让他人知道他正在下载的文件是不是就是他所想要的。尤其是在文件的其他属性被更改之后(如名称等)这个值就更显得重要。而且服务器还提供了,这个文件当前所在的用户的地址,端口等信息,这样emule就知道到哪里去下载了。

      一般来讲我们要搜索一个文件,emule在得到了这个信息后,会向被添加的服务器发出请求,要求得到有相同hash值的文件。而服务器则返回持有这个文件的用户信息。这样我们的客户端就可以直接的和拥有那个文件的用户沟通,看看是不是可以从他那里下载所需的文件。

      对于emule中文件的hash值是固定的,也是唯一的,它就相当于这个文件的信息摘要,无论这个文件在谁的机器上,他的hash值都是不变的,无论过了多长时间,这个值始终如一,当我们在进行文件的下载上传过程中,emule都是通过这个值来确定文件。

      那么什么是userhash呢?

      道理同上,当我们在第一次使用emule的时候,emule会自动生成一个值,这个值也是唯一的,它是我们在emule世界里面的标志,只要你不卸载,不删除config,你的userhash值也就永远不变,积分制度就是通过这个值在起作用,emule里面的积分保存,身份识别,都是使用这个值,而和你的id和你的用户名无关,你随便怎么改这些东西,你的userhash值都是不变的,这也充分保证了公平性。其实他也是一个信息摘要,只不过保存的不是文件信息,而是我们每个人的信息。

      那么什么是hash文件呢?

      我们经常在emule日志里面看到,emule正在hash文件,这里就是利用了hash算法的文件校验性这个功能了,文章前面已经说了一些这些功能,其实这部分是一个非常复杂的过程,目前在ftp,bt等软件里面都是用的这个基本原理,emule里面是采用文件分块传输,这样传输的每一块都要进行对比校验,如果错误则要进行重新下载,这期间这些相关信息写入met文件,直到整个任务完成,这个时候part文件进行重新命名,然后使用move命令,把它传送到incoming文件里面,然后met文件自动删除,所以我们有的时候会遇到hash文件失败,就是指的是met里面的信息出了错误不能够和part文件匹配,另外有的时候开机也要疯狂hash,有两种情况一种是你在第一次使用,这个时候要hash提取所有文件信息,还有一种情况就是上一次你非法关机,那么这个时候就是要进行排错校验了。

      关于hash的算法研究,一直是信息科学里面的一个前沿,尤其在网络技术普及的今天,他的重要性越来越突出,其实我们每天在网上进行的信息交流安全验证,我们在使用的操作系统密钥原理,里面都有它的身影,特别对于那些研究信息安全有兴趣的朋友,这更是一个打开信息世界的钥匙,他在hack世界里面也是一个研究的焦点。

      一般的线性表、树中,记录在结构中的相对位置是随机的即和记录的关键字之间不存在确定的关系,在结构中查找记录时需进行一系列和关键字的比较。这一类查找方法建立在“比较”的基础上,查找的效率与比较次数密切相关。理想的情况是能直接找到需要的记录,因此必须在记录的存储位置和它的关键字之间建立一确定的对应关系f,使每个关键字和结构中一个唯一的存储位置相对应。因而查找时,只需根据这个对应关系f找到给定值K的像f(K)。若结构中存在关键字和K相等的记录,则必定在f(K)的存储位置上,由此不需要进行比较便可直接取得所查记录。在此,称这个对应关系f为哈希函数,按这个思想建立的表为哈希表(又称为杂凑法或散列表)。

      哈希表不可避免冲突(collision)现象:对不同的关键字可能得到同一哈希地址 即key1≠key2,而hash(key1)=hash(key2)。具有相同函数值的关键字对该哈希函数来说称为同义词(synonym)。因此,在建造哈希表时不仅要设定一个好的哈希函数,而且要设定一种处理冲突的方法。可如下描述哈希表:根据设定的哈希函数H(key)和所选中的处理冲突的方法,将一组关键字映象到一个有限的、地址连续的地址集(区间)上并以关键字在地址集中的“象”作为相应记录在表中的存储位置,这种表被称为哈希表。

      对于动态查找表而言,1) 表长不确定;2)在设计查找表时,只知道关键字所属范围,而不知道确切的关键字。因此,一般情况需建立一个函数关系,以f(key)作为关键字为key的录在表中的位置,通常称这个函数f(key)为哈希函数。(注意:这个函数并不一定是数学函数)

      哈希函数是一个映象,即:将关键字的集合映射到某个地址集合上,它的设置很灵活,只要这个地址集合的大小不超出允许范围即可。

      现实中哈希函数是需要构造的,并且构造的好才能使用的好。

      用途:加密,解决冲突问题。

      用途很广,比特精灵中就使用了哈希函数,你可以自己看看。

      具体可以学习一下数据结构和算法的书。

    字符串哈希函数

      (著名的ELFhash算法)

      int ELFhash(char *key)

      {

      unsigned long h=0;

      while(*key)

      {

      h=(h<<4)+*key++;

      unsigned long g=h&0Xf0000000L;

      if(g)

      h^=g>>24;

      h&=~g;

      }

      return h%MOD;

      }

     

    Hash表(C++实现)

    哈希表的几个概念:

    映像:由哈希函数得到的哈希表是一个映像。

    冲突:如果两个关键字的哈希函数值相等,这种现象称为冲突。

    处理冲突的几个方法:

    1、开放地址法:用开放地址处理冲突就是当冲突发生时,形成一个地址序列,沿着这个序列逐个深测,直到找到一个“空”的开放地址,将发生冲突的关键字值存放到该地址中去。

    例如:hash(i)=(hash(key)+d(i)) MOD m (i=1,2,3,......,k(k<m-1)) d为增量函数,d(i)=d1,d2,d3,...,dn-1

    根据增量序列的取法不同,可以得到不同的开放地址处理冲突探测方法。

    有线性探测法、二次方探测法、伪随机探测法。

    2、链地址法:把所有关键字为同义词的记录存储在一个线性链表中,这个链表成为同义词链表,即把具有相同哈希地址的关键字值存放在同义链表中。

    3、再哈希表:费时间的一种方法

    下面是代码:

    文件"myhash.h"

    view plain

    1. #include<iostream>  
    2. using namespace std;  
    3.   
    4. typedef int KeyType; //设关键字域为整形,需要修改类型时,只需修改这里就可以  
    5. const int NULLKEY=0; //NULLKEY表示该位置无值  
    6. int c=0; //用来统计冲突次数  
    7.   
    8. struct Elemtype //数据元素类型  
    9. {  
    10.     KeyType key;  
    11.     int ord;   
    12. };  
    13.   
    14. int hashsize[]={11,19,29,37,47}; //hash表容量递增表  
    15. int Hash_length=0;//hash表表长  
    16.   
    17. class HashTable  
    18. {  
    19. private:  
    20.     Elemtype *elem; //数据元素数组,动态申请  
    21.     int count;// 当前数据元素个数  
    22.     int size; //决定hash表的容量为第几个,hashsize[size]为当前hash容量  
    23. public:  
    24.   
    25.     int Init_HashTable() //构造一个空hash表  
    26.     {  
    27.         int i;  
    28.         count=0;  
    29.         size=0; //初始化容量为hashsize[0]=11  
    30.         Hash_length=hashsize[0];  
    31.         elem=new Elemtype[Hash_length];  
    32.         if(!elem)  
    33.         {  
    34.             cout<<"内存申请失败"<<endl;  
    35.             exit(0);  
    36.         }  
    37.         for(i=0;i<Hash_length;i++)  
    38.             elem[i].key=NULLKEY;  
    39.         return 1;  
    40.     }  
    41.   
    42.     void Destroy_HashTable()  
    43.     {  
    44.         delete[]elem;  
    45.         elem=NULL;  
    46.         count=0;  
    47.         size=0;  
    48.     }  
    49.   
    50.     unsigned Hash(KeyType k) //hash函数的一种(取模法)  
    51.     {  
    52.         return k%Hash_length;  
    53.     }  
    54.   
    55.     void Collision(int &p,int d) //解决冲突  
    56.     {  
    57.         p=(p+d)%Hash_length; //采用开放地址法里的线性探测  
    58.     }  
    59.   
    60.     bool Search_Hash(KeyType k,int &p) //查找  
    61.     {  
    62.         //在开放地址hash表中查找关键字等于k的元素  
    63.         //若找到用p表示待查数据,查找不成功时,p指向的是可插入地址  
    64.         c=0;  
    65.         p=Hash(k); //求hash地址  
    66.         while(elem[p].key!=NULLKEY && elem[p].key!=k)  
    67.         {  
    68.             c++;  
    69.             if(c<Hash_length)  
    70.                 Collision(p,c);  
    71.             else  
    72.                 return 0; //表示查找不成功  
    73.         }  
    74.         if(elem[p].key==k)  
    75.             return 1;  
    76.         else  
    77.             return 0;  
    78.   
    79.     }  
    80.   
    81.     int Insert_Hash(Elemtype e) //插入  
    82.     {  
    83.         //在查找不成功的情况下将k插入到hash表中  
    84.         int p;  
    85.         if(Search_Hash(e.key,p))  
    86.             return -1; //表示该元素已在hash表中  
    87.         else if(c<hashsize[size]/2) //冲突次数未达到上限  
    88.         {  
    89.             //插入e  
    90.             elem[p]=e;  
    91.             count++;  
    92.             return 1;  
    93.         }  
    94.         else  
    95.             ReCreate_HashTable(); // 重建hash表  
    96.         return 0; //插入失败  
    97.     }  
    98.   
    99.     void ReCreate_HashTable() //重建hash表  
    100.     {  
    101.         int i,count2=count;  
    102.         Elemtype *p,*elem2=new Elemtype[count];  
    103.         p=elem2;  
    104.         cout<<"____重建hash表_____"<<endl;  
    105.         for(i=0;i<Hash_length;i++) //将原有元素暂存到elem2中  
    106.             if(elem[i].key!=NULLKEY)  
    107.                 *p++=*(elem+i);  
    108.         count=0;  
    109.         size++; //hash容量增大  
    110.         Hash_length=hashsize[size];  
    111.         p=new Elemtype[Hash_length];  
    112.         if(!p)  
    113.         {  
    114.             cout<<"空间申请失败"<<endl;  
    115.             exit(0);  
    116.         }  
    117.         elem=p;  
    118.         for(i=0;i<Hash_length;i++)  
    119.             elem[i].key=NULLKEY;  
    120.         for(p=elem2;p<elem2+count2;p++) //将原有元素放回新表  
    121.             Insert_Hash(*p);  
    122.     }  
    123.   
    124.     void Traverse_HashTable()  
    125.     {  
    126.         cout<<"哈希地址0->"<<Hash_length-1<<endl;  
    127.         for(int i=0;i<Hash_length;i++)  
    128.             if(elem[i].key!=NULLKEY)  
    129.                 cout<<"元素的关键字值和它的标志分别是:"<<elem[i].key<<"  "<<elem[i].ord<<endl;  
    130.   
    131.     }  
    132.   
    133.     void Get_Data(int p)  
    134.     {  
    135.         cout<<"元素的关键字值和它的标志分别是:"<<elem[p].key<<"  "<<elem[p].ord<<endl;  
    136.     }  
    137.       
    138. };  

    测试函数"main.cpp"

    view plain

    1. #include"myhash.h"  
    2.   
    3. int main()  
    4. {  
    5.     Elemtype r[12]={{17,1},{60,2},{29,3},{38,4},{1,5},{2,6},{3,7},{4,8},{5,9},{6,10},{7,11},{8,12}};  
    6.     HashTable H;  
    7.     int i,p,j;  
    8.     KeyType k;  
    9.     H.Init_HashTable();  
    10.     for(i=0;i<11;i++) //插入前11个记录  
    11.     {  
    12.         j=H.Insert_Hash(r[i]);  
    13.         if(j==-1)  
    14.             cout<<"表中已有关键字为"<<r[i].key<<"  "<<r[i].ord<<"的记录"<<endl;  
    15.     }  
    16.   
    17.     cout<<"按哈希地址顺序遍历哈希表"<<endl;  
    18.     H.Traverse_HashTable();  
    19.     cout<<endl;  
    20.   
    21.     cout<<"输入要查找的记录的关键字:";  
    22.     cin>>k;  
    23.     j=H.Search_Hash(k,p);  
    24.     if(j==1)  
    25.         H.Get_Data(p);  
    26.     else  
    27.         cout<<"无此记录"<<endl;  
    28.   
    29.     j=H.Insert_Hash(r[11]); //插入最后一个元素  
    30.     if(j==0)  
    31.     {  
    32.         cout<<"插入失败"<<endl;  
    33.         cout<<"需要重建哈希表才可以插入"<<endl;  
    34.         cout<<"____重建哈希表____"<<endl;  
    35.         H.Insert_Hash(r[i]); //重建后重新插入  
    36.     }  
    37.   
    38.     cout<<"遍历重建后的哈希表"<<endl;  
    39.     H.Traverse_HashTable();  
    40.     cout<<endl;  
    41.   
    42.     cout<<"输入要查找的记录的关键字:";  
    43.     cin>>k;  
    44.     j=H.Search_Hash(k,p);  
    45.     if(j==1)  
    46.         H.Get_Data(p);  
    47.     else  
    48.         cout<<"该记录不存在"<<endl;  
    49.   
    50.     cout<<"____销毁哈希表____"<<endl;  
    51.     H.Destroy_HashTable();  
    52.   
    53.     return 0;  
    54. }  

    测试结果:

    view plain

    1. 按哈希地址顺序遍历哈希表  
    2. 哈希地址0->10  
    3. 元素的关键字值和它的标志分别是:5  9  
    4. 元素的关键字值和它的标志分别是:1  5  
    5. 元素的关键字值和它的标志分别是:2  6  
    6. 元素的关键字值和它的标志分别是:3  7  
    7. 元素的关键字值和它的标志分别是:4  8  
    8. 元素的关键字值和它的标志分别是:60  2  
    9. 元素的关键字值和它的标志分别是:17  1  
    10. 元素的关键字值和它的标志分别是:29  3  
    11. 元素的关键字值和它的标志分别是:38  4  
    12. 元素的关键字值和它的标志分别是:6  10  
    13. 元素的关键字值和它的标志分别是:7  11  
    14.   
    15. 输入要查找的记录的关键字:5  
    16. 元素的关键字值和它的标志分别是:5  9  
    17. ____重建hash表_____  
    18. 插入失败  
    19. 需要重建哈希表才可以插入  
    20. ____重建哈希表____  
    21. 遍历重建后的哈希表  
    22. 哈希地址0->18  
    23. 元素的关键字值和它的标志分别是:38  4  
    24. 元素的关键字值和它的标志分别是:1  5  
    25. 元素的关键字值和它的标志分别是:2  6  
    26. 元素的关键字值和它的标志分别是:3  7  
    27. 元素的关键字值和它的标志分别是:4  8  
    28. 元素的关键字值和它的标志分别是:5  9  
    29. 元素的关键字值和它的标志分别是:60  2  
    30. 元素的关键字值和它的标志分别是:6  10  
    31. 元素的关键字值和它的标志分别是:7  11  
    32. 元素的关键字值和它的标志分别是:8  12  
    33. 元素的关键字值和它的标志分别是:29  3  
    34. 元素的关键字值和它的标志分别是:17  1  
    35.   
    36. 输入要查找的记录的关键字:7  
    37. 元素的关键字值和它的标志分别是:7  11  
    38. ____销毁哈希表____  
    39. Press any key to continue  
  • 相关阅读:
    tinymce原装插件源码分析(二)-link
    tinymce原装插件源码分析(一)-hr
    pyinstall 常见错误
    matlab Time-domain analysis 渐进式或者实时获取仿真值
    初识python和pycharm
    自定义指令详解
    Vue核心知识一览
    多维数组 转化为 一维数组
    js面试之数组的几个不low操作
    js如何操作或是更改sass里的变量
  • 原文地址:https://www.cnblogs.com/skyofbitbit/p/2681708.html
Copyright © 2020-2023  润新知