@TOC




一:哈希表   

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

        给定表M,存在函数f(key),对任意给定的关键字值key,带入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希表(Hash table),函数f(key)为哈希函数(hash Function)。

        缓存就和hash表有关,Memcached这一缓存技术其实在它的内部就维护了一张很大的哈希表。

        数组的存储结构(优点在于查找方便快速):

深入理解Java中的HashMap

        数组:在内存当中连续开辟的n个单元空间;根据下表可以快速找到所找的元素,但是如果是给定元素去找数组中对应的元素优势就没有那么明显了;插入和删除效率很低

        链表(单向链表)的存储结构(优点在于存储的增删操作不会移动其他位置的元素):

深入理解Java中的HashMap

        链表:(单向链表和双向链表)以单项链表为例,最后一个next为null;插入和删除效率高,但是查找元素效率低

        哈希表的存储结构:数组+链表的存储结构即:顺序存储结构 + 链式存储结构(集两者优点于一身,不仅可以快速存还可以快速取):

深入理解Java中的HashMap

        哈希表:本质主题结构也是一个数组;优势通过下标去找可以很快找到;但是会和链表结合,集两者的优点统一起来;哈希表集合了数组和链表的优点于一身,不仅可以快速存且可以快速取(顺序的存储结构+链式的存储结构),缺点:不支持排序,一般比线性表存储需要更多的空间,并且记录的关键字不能重复。

        Hash函数:hash表采用一个映射函数 f :key -> address 将关键字映射到该记录在表中的存储位置,从而在想要查找该记录时,可以直接根据关键字和映射关系计算出该记录在表中的存储位置,通常情况下,这种映射关系称作为Hash函数,而通过hash函数和关键字计算出来的存储位置(注:这里的存储位置只是表中的存储位置,并非实际的物理地址)称作为Hash地址。

        哈希函数的构造方法:

        1)直接定址法:取关键字或者关键字的某个线性函数为Hash地址,即address(key) = a * key + b;

        2)平方取中法:对关键字进行平方运算,然后取结果的中间几位作为Hash地址;address(key) = a * a  取它结果的中间值。

        3)折叠法:对关键字拆分成几部分,然后将这几部分组合在一起,以特定的方式进行转化形成Hash地址,比如:abc:address(key) = a + b + c;

        4)除留取余法:如果知道Hash表的最大长度为N,可以取不大于N的最大质数p,然后对关键字进行取余运算,address(key)= key % p;(最常用的方法)

        哈希表中解决冲突的方法:

        1)开放地址法(即当一个关键字和另一个关键字发生冲突时,使用某种探测技术在Hash表中形成一个探测序列,然后沿着这个探测序列依次查找下去,当碰到一个空的单元时,则插入其中。即哈希表采用的是线性的存储结构);

        2)链地址法(采用数组和链表相结合的方法,将Hash地址相同的记录存储在一张线性链表中,而每张链表的表头的序号即为计算得到的Hash地址,就如上图所画的示意图,这也是现在大多数实现HashMap采用的方法);

        Hash表的平均查找长度:

        hash表的平均查找长度有两种:包括查找成功时的平均查找长度和查找失败时的平均查找长度;

        查找成功时的平均查找长度 = 表中的每个元素查找成功时的比较次数之和  / 表中元素的个数;

        查找不成功时的平均查找长度 = 在表中每一种查找不成功时的比较次数之和 / 表的长度;即可以理解为向表中插入一个新的元素,该元素在每个位置都有可能,然后计算出在每个位置能够插入时需要比较的次数 / 表的长度;

        Demo:一组关键字{22,13,4,6,9,10,7,8},表的长度为15,Hash函数取除留取余法的方式即address(key) = key % 15;则关键字在表中的存储如下图所示:

深入理解Java中的HashMap

        这里有一个概念:负载因子 = 表中的记录数 / 哈希表的长度,如果负载因子越小,说明表中空单元还有很多,则发生冲突的可能性越小;而负载因子越大,则发生冲突的可能性就越大,在查找时所耗费的时间就越多。因此,hash表的平均查找长度和负载因此有关。有相关文献证明当负载因子在0.5的时候,hash的性能能有达到最优。但是一般规定负载因子取值为0.75。

二:JDK中的HashMap实现原理

        在JDK1.6,JDK1.7中,HashMap采用位桶+ 链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里,但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶 + 链表 + 红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

        首先有一个每个元素都是链表(可能表述不准确)的数组,当添加一个元素(key - value)时,就首先计算元素 key 的 hash 值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,它们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。

深入理解Java中的HashMap

深入理解Java中的HashMap

        上面的两幅图结合着理解,你就可以很好地理解了hashMap的存储结构,它很好的展示了HashMap的实现原理。

        哈希函数 f 设计的主要目的:找到存储位置(哈希函数的优劣  位置散列分布均匀  而且冲突小)深入理解Java中的HashMap

        哈希函数 f 设计的主要优点:第一,存或者取都可以通过 f 哈希函数实现,并且时间的复杂度和操作的复杂度都是O(1),即寻找的时间复杂度只经过一个步骤,不管是存还是取都可以通过 f 哈希函数一步到位;

        哈希函数设计的不好:位置散列容易冲突   后果:数组其他位置没有利用到,浪费内存空间;链表太长  快存或者快取都要经历大量的逻辑判断,效率低下

        哈希函数是一个笼统地叫法,函数算法是哈希函数里面的一个步骤

三、JDK1.7中HashMap代码解读

        1:阅读源码可知,HashMap的底层是一个数组结构,数组的每一项又是一个链表,当新建一个HashMap的时候就会初始化一个数组。而每个数组中的元素就是一个Entry,Entry里面包括key、value对,还有Entry<K, V> next以及hash,所以每个Map.Entry其实就是一个key-value对,它持有一个指向下一个元素的引用Entry<K, V> next,这就构成了链表。其中源码如下:

  1. static class Entry<K,V> implements Map.Entry<K,V> {
  2. final K key;//键
  3. V value;//值
  4. Entry<K,V> next;//指向下一个元素的引用
  5. int hash;//hash值
  6. /**
  7. * Creates new entry.
  8. */
  9. Entry(int h, K k, V v, Entry<K,V> n) {
  10. value = v;
  11. next = n;
  12. key = k;
  13. hash = h;
  14. }
  15. public final K getKey() {
  16. return key;
  17. }
  18. public final V getValue() {
  19. return value;
  20. }
  21. public final V setValue(V newValue) {
  22. V oldValue = value;
  23. value = newValue;
  24. return oldValue;
  25. }
  26. public final boolean equals(Object o) {
  27. if (!(o instanceof Map.Entry))
  28. return false;
  29. Map.Entry e = (Map.Entry)o;
  30. Object k1 = getKey();
  31. Object k2 = e.getKey();
  32. if (k1 == k2 || (k1 != null && k1.equals(k2))) {
  33. Object v1 = getValue();
  34. Object v2 = e.getValue();
  35. if (v1 == v2 || (v1 != null && v1.equals(v2)))
  36. return true;
  37. }
  38. return false;
  39. }
  40. public final int hashCode() {
  41. return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
  42. }
  43. public final String toString() {
  44. return getKey() + "=" + getValue();
  45. }
  46. /**
  47. * This method is invoked whenever the value in an entry is
  48. * overwritten by an invocation of put(k,v) for a key k that's already
  49. * in the HashMap.
  50. */
  51. void recordAccess(HashMap<K,V> m) {
  52. }
  53. /**
  54. * This method is invoked whenever the entry is
  55. * removed from the table.
  56. */
  57. void recordRemoval(HashMap<K,V> m) {
  58. }
  59. }

        2、HashMap的存取实现:

        HashMap存储元素的思想是(首先在讲解hashMap之前先普及一个知识:HashMap可以存储一个key为null的值,可以存储多个key!=null而value为null的值,但是HashTable不能):在HashMap中put()元素的时候,先判断key是否为null,为null,在存储在规定的位置(table[0]处),否则根据key的hashCode()方法计算hash值,根据hash值得到这个元素在数组中的位置:如果数组中的位置已经存放其他元素了,那么在这个位置将会用链表的形式存放,首先在table[i]处对应的链表进行遍历,判断已经存在的链表中的Entry实体中是否有与这个元素中的key的hash相同并且key相同,如果有则替换原来oldValue,如果没有,则把这个元素放在链表的链头,即头部插入法,最先加入的在链尾,最后加入的在链首;如果数组中的位置没有存放Entry实体,就直接新建一个Entry实体,将该元素放到此数组中的该位置上(即key.hashCode()得到hashCode------>hash(hashCode)得到hash值h------> h&(length-1)------>得到数组的最终索引)。下面按代码详细讲解:

        代码一:put()方法:存元素

  1. public V put(K key, V value) {
  2. //如果数组为EMPTY_TABLE即数组是否为null,是则初始化table
  3. if (table == EMPTY_TABLE) {
  4. inflateTable(threshold);
  5. }
  6. //如果key为空值,执行putForNullKey()方法,将value放置在table的第一个位置
  7. if (key == null)
  8. return putForNullKey(value);
  9. //根据key的hashCode,计算的hash值
  10. int hash = hash(key);
  11. //搜索指定hash值在对应table中的索引
  12. int i = indexFor(hash, table.length);
  13. //如果table[i]的Entry不为null,通过循环不断遍历e元素的下一个元素,判断是否有元素的hash相同和key相同的
  14. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  15. Object k;
  16. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  17. V oldValue = e.value;//如果hash值相同,key相同,则新value替换老的value,返回老的value。
  18. e.value = value;
  19. e.recordAccess(this);
  20. return oldValue;
  21. }
  22. }
  23. //如果table[i]的Entry为空,说明table[i]没有Entry
  24. modCount++;
  25. //将hash,key,value填到table[i]处
  26. addEntry(hash, key, value, i);
  27. return null;
  28. }

        public V put(K key,V value)方法,存储元素;首先判断table是否为null,如果为null,则初始化数组,接下来判断元素的key是否为null,如果为null,则执行putForNullKey()方法;

  1. private V putForNullKey(V value) {
  2. //在table[0]处的链表进行循环遍历
  3. for (Entry<K,V> e = table[0]; e != null; e = e.next) {
  4. //存在元素时,判断元素的key是否为null,如果是则用新值替换老值
  5. if (e.key == null) {
  6. V oldValue = e.value;
  7. e.value = value;
  8. e.recordAccess(this);
  9. return oldValue;
  10. }
  11. }
  12. //如果table[0]不存在元素
  13. modCount++;
  14. //添加key为null的元素
  15. addEntry(0, null, value, 0);
  16. return null;
  17. }
        private V putForNullKey(V value)方法,存储key为null的元素;判断table[0]处的是否有元素,有则用新值代替老值,如果没有,则直接调用addEntry()方法,进行存储key为null的元素。

  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2. //判断table[i]位置存储的链表是否大于阈值并且判断table[i]不为null(扩容条件)
  3. if ((size >= threshold) && (null != table[bucketIndex])) {
  4. //进行扩容
  5. resize(2 * table.length);
  6. //重新计算hash值
  7. hash = (null != key) ? hash(key) : 0;
  8. //重新得到bucketIndex
  9. bucketIndex = indexFor(hash, table.length);
  10. }
  11. //创建Entry实体添加此元素
  12. createEntry(hash, key, value, bucketIndex);
  13. }
        void addEntry(int hash,K key,V value,int bucketIndex)方法,添加Entry实体存储元素;判断是否需要扩容,如果需要则进行扩容,长度变为2倍,调用resize()方法进行扩容,原来的元素进行重新散列存储。如果不需要则直接调用createEntry()方法,添加元素。

  1. void createEntry(int hash, K key, V value, int bucketIndex) {
  2. Entry<K,V> e = table[bucketIndex];
  3. table[bucketIndex] = new Entry<>(hash, key, value, e);
  4. size++;
  5. }
       至此,当key为null的value已经存储在table[0]处。下面来讨论key不为null时的元素存储。在key不等于null时,看上面的put()方法,我们知道首先需要计算这个元素的hash值,根据这个元素的key的hashCode。

  1. final int hash(Object k) {
  2. //首先选取一个hashSeed;
  3. //hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this): 0;
  4. int h = hashSeed;
  5. //判断h是否为0并且判断k的是否为String类的实例
  6. if (0 != h && k instanceof String) {
  7. //是,则k返回hash值
  8. return sun.misc.Hashing.stringHash32((String) k);
  9. }
  10. //不是,则进行hash值的计算
  11. h ^= k.hashCode();
  12. h ^= (h >>> 20) ^ (h >>> 12);
  13. //返回hash值
  14. return h ^ (h >>> 7) ^ (h >>> 4);
  15. }
        final int hash(Object k),调用这个方法返回对应的hash值。其中元素key的hashCode是JDK开发者定义的,不可见。有兴趣可以自行查阅资料。当返回对应的hash值时,再根据indexFor()方法,进行元素对应位置的寻取。

  1. /**
  2. * Returns index for hash code h.
  3. */
  4. static int indexFor(int h, int length) {
  5. // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
  6. return h & (length-1);
  7. }
        static int indexFor(int h,int length);根据hash值由indexFor()方法,进行计算得到数组的索引。这里声明的“assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2”,这里数组长度为什么必须是2的次幂呢?

        举个例子,JDK默认的数组长度为:1<<4即为16;Length:2的次幂:2^4=16;我们就拿数组长度为16和15来进行计算解释说明为什么必须是2的次幂:

        数组长度为16:length - 1 = 15 ------>二进制为: 1111;数组长度为15:length - 1 = 14 ------>二进制为:1110;假设来了两个元素,经过计算它们的key的hashCode得到hash值为:6(二进制为:0110)和7(二进制为:0111),下表是它们根据indexFor()计算后得到的数组索引:

深入理解Java中的HashMap

        由上表可以得知:当hash值为6和7时,它们和14的二进制进行“与”的时候,产生了相同的结果,产生了碰撞,即它们最后会在数组中的相同的位置即同一个bucket内形成链表,这样在查找这个bucket中的元素时,就会增加查询量,降低查询效率。并且数组中0001、0011、0101、0111、1001、1011、1101所对应的数组位置永远不能存放元素,浪费存储空间,增加了碰撞几率,也增加了每一个bucket中的Entry数量,这样在查询时会降低查询效率。即所有的决定因素都在低位,而数组长度为16可以很好的避免这些问题。使元素散列均匀,只有hash值相同的两个元素才能方法数组中的同一个位置上,形成链表。故数组长度为2的次幂的时候,不同的key.hashCode()在进行hash()后得到相同的位置几率会大大降低,使元素散列均匀,减少碰撞,这样元素在查找效率就会提高。总结:index的位置不会超过数组的下标,也不会引起数组越界;index的位置更加均匀。

        接着上面的put(K key, V value)进行说明,在调用indexFor()得到数组的索引位置以后,接下来就是在数组所对应的bucket链表中进行遍历,判断是否存在hash值相同和key相同的元素,如果存在就用新value代替原来的老value,如果不存在就调用addEntry(hash, key, value, i)方法进行元素的存储。(这里就说一下上面存储key为null时与key不为null时有差别的方法,不再一步一步进行介绍)

  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2. //判断table[i]位置存储的链表是否大于阈值并且判断table[i]不为null(扩容条件)
  3. if ((size >= threshold) && (null != table[bucketIndex])) {
  4. //进行扩容
  5. resize(2 * table.length);
  6. //重新计算hash值
  7. hash = (null != key) ? hash(key) : 0;
  8. //重新得到bucketIndex
  9. bucketIndex = indexFor(hash, table.length);
  10. }
  11. //创建Entry实体添加此元素
  12. createEntry(hash, key, value, bucketIndex);
  13. }
        这个方法上面已经讲解过了,这里重点说一下resize()方法。

  1. void resize(int newCapacity) {
  2. //声明Entry[]数组oldTable,把table地址赋值给oldTable;
  3. Entry[] oldTable = table;
  4. //获取数组长度
  5. int oldCapacity = oldTable.length;
  6. //判断数组长度是否为MAXIMUM_CAPACITY容量
  7. if (oldCapacity == MAXIMUM_CAPACITY) {
  8. //把Integer.MAX_VALUE赋值给threshold
  9. threshold = Integer.MAX_VALUE;
  10. return;
  11. }
  12. //声明新的Entry[]数组实体
  13. Entry[] newTable = new Entry[newCapacity];
  14. //将扩容之前的转到扩容之后的HashMap中
  15. transfer(newTable, initHashSeedAsNeeded(newCapacity));
  16. //把地址赋值给原来声明的数组地址
  17. table = newTable;
  18. //重新定义新的阈值
  19. threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  20. }
        上面是进行扩容的方法这里说一下扩容的条件,扩容:第一个有冲突且使用数组的位置已经大于数组的所有位置*扩容值(0.75);原来的长度*2    保证扩容之后的length-1   还是保证低位为1。

  1. /**
  2. * Transfers all entries from current table to newTable.
  3. */
  4. void transfer(Entry[] newTable, boolean rehash) {
  5. int newCapacity = newTable.length;
  6. for (Entry<K,V> e : table) {
  7. while(null != e) {
  8. Entry<K,V> next = e.next;
  9. if (rehash) {
  10. e.hash = null == e.key ? 0 : hash(e.key);
  11. }
  12. int i = indexFor(e.hash, newCapacity);
  13. e.next = newTable[i];
  14. newTable[i] = e;
  15. e = next;
  16. }
  17. }
  18. }
        transfer()方法,对原来的进行重新散列到扩容以后的新的HashMap中。
        3、hashMap读取Entry实体。
  1. //根据key获取元素
  2. public V get(Object key) {
  3. //取key为null的Entry实体元素
  4. if (key == null)
  5. return getForNullKey();
  6. //取key不为null的Entry实体元素
  7. Entry<K,V> entry = getEntry(key);
  8. return null == entry ? null : entry.getValue();
  9. }
        public V get(Object key);读取HashMap中的元素,分为两种情况,读取key为null的Entry实体元素;读取key不为null的Entry实体元素。getForNullKey()用来读取key为null的Entry实体元素。
  1. private V getForNullKey() {
  2. if (size == 0) {
  3. return null;
  4. }
  5. for (Entry<K,V> e = table[0]; e != null; e = e.next) {
  6. if (e.key == null)
  7. return e.value;
  8. }
  9. return null;
  10. }
        getEntry()读取key不为null的Entry实体
  1. final Entry<K,V> getEntry(Object key) {
  2. if (size == 0) {
  3. return null;
  4. }
  5. int hash = (key == null) ? 0 : hash(key);
  6. for (Entry<K,V> e = table[indexFor(hash, table.length)];
  7. e != null;
  8. e = e.next) {
  9. Object k;
  10. if (e.hash == hash &&
  11. ((k = e.key) == key || (key != null && key.equals(k))))
  12. return e;
  13. }
  14. return null;
  15. }

重写equals方法需同时重写hashCode方法


欢迎使用Markdown编辑器

你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

新的改变

我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:

  1. 全新的界面设计 ,将会带来全新的写作体验;
  2. 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
  3. 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
  4. 全新的 KaTeX数学公式 语法;
  5. 增加了支持甘特图的mermaid语法1 功能;
  6. 增加了 多屏幕编辑 Markdown文章功能;
  7. 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
  8. 增加了 检查列表 功能。

功能快捷键

撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + Shift + B
斜体:Ctrl/Command + Shift + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G

合理的创建标题,有助于目录的生成

直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。

如何改变文本的样式

强调文本 强调文本

加粗文本 加粗文本

标记文本

删除文本

引用文本

H2O is是液体。

210 运算结果是 1024.

插入链接与图片

链接: link.

图片: 深入理解Java中的HashMap

带尺寸的图片: 深入理解Java中的HashMap

当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。

如何插入一段漂亮的代码片

博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.

// An highlighted block
var foo = 'bar';

生成一个适合你的列表

  • 项目
    • 项目
      • 项目
  1. 项目1
  2. 项目2
  3. 项目3
  • 计划任务
  • 完成任务

创建一个表格

一个简单的表格是这么创建的:

项目 Value
电脑 $1600
手机 $12
导管 $1

设定内容居中、居左、居右

使用:---------:居中
使用:----------居左
使用----------:居右

第一列 第二列 第三列
第一列文本居中 第二列文本居右 第三列文本居左

SmartyPants

SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:

TYPE ASCII HTML
Single backticks 'Isn't this fun?' ‘Isn’t this fun?’
Quotes "Isn't this fun?" “Isn’t this fun?”
Dashes -- is en-dash, --- is em-dash – is en-dash, — is em-dash

创建一个自定义列表

Markdown
Text-to-HTML conversion tool
Authors
John
Luke

如何创建一个注脚

一个具有注脚的文本。2

注释也是必不可少的

Markdown将文本转换为 HTML

KaTeX数学公式

您可以使用渲染LaTeX数学表达式 KaTeX:

Gamma公式展示 Γ(n)=(n1)!nN\Gamma(n) = (n-1)!\quad\forall n\in\mathbb N 是通过欧拉积分

Γ(z)=0tz1etdt&ThinSpace;. \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,.

你可以找到更多关于的信息 LaTeX 数学表达式here.

新的甘特图功能,丰富你的文章

Mon 06Mon 13Mon 20已完成 进行中 计划一 计划二 现有任务Adding GANTT diagram functionality to mermaid
  • 关于 甘特图 语法,参考 这儿,

UML 图表

可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图::

张三李四王五你好!李四, 最近怎么样?你最近怎么样,王五?我很好,谢谢!我很好,谢谢!李四想了很长时间,文字太长了不适合放在一行.打量着王五...很好... 王五, 你怎么样?张三李四王五

这将产生一个流程图。:

链接
长方形
圆角长方形
菱形
  • 关于 Mermaid 语法,参考 这儿,

FLowchart流程图

我们依旧会支持flowchart的流程图:

Created with Raphaël 2.2.0开始我的操作确认?结束yesno
  • 关于 Flowchart流程图 语法,参考 这儿.

导出与导入

导出

如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。

导入

如果你想加载一篇你写过的.md文件或者.html文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。


  1. mermaid语法说明 ↩︎

  2. 注脚的解释 ↩︎

相关文章: