IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> ConcurrentHashMap原理解析 -> 正文阅读

[数据结构与算法]ConcurrentHashMap原理解析

由来

HashMap

HashMap是线程不安全的,在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的。

HashTable

HashTable和HashMap的实现原理几乎一样,差别无非是1.HashTable不允许key和value为null;2.HashTable是线程安全的。但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差
HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想。

ConcurrentHashMap

ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现(若对HashMap的实现原理还不甚了解,可参考我的另一篇文章HashMap实现原理及源码分析),ConcurrentHashMap在并发编程的场景中使用频率非常之高

应用场景

当有一个大数组时需要在多个线程共享时就可以考虑是否把它给分层多个节点了,避免大锁。并可以考虑通过hash算法进行一些模块定位。
其实不止用于线程,当设计数据表的事务时(事务某种意义上也是同步机制的体现),可以把一个表看成一个需要同步的数组,如果操作的表数据太多时就可以考虑事务分离了(这也是为什么要避免大表的出现),比如把数据进行字段拆分,水平分表等.

解决哈希冲突的方案

1.开放寻址

如果i被占用就探查 i+1,i+2 i+…的位置

链式寻址法

hash表的每个位置都连接一个链表 当发生hash冲突时 冲突的元素将会加入这个位置的链表的最后

再哈希法

提供多个不同的hash 函数 当发生冲突时 使用第二个第三个等

put方法原理解析

1.根据 key的 code 计算hash值
2.如果 table是空 则进行初始化
3.如果 table 不是空 hash 值来计算 key 在table 中的下标 如果没有值则直接添加进去
4.如果当前计算的下标有值 对当前数组位置的节点进行加锁
从链表头部 开始遍历 如果存在相同的key 则进行修改对应的value 如果没有相同的key 则构建一个node 从尾部插入链表
5.如果是正在扩容的情况下 则进行多线程协助扩容
当数组长度小于等于64 并且链表长度大于等于8时 优先对数组进行扩容
当数组长度大于64 并且链表长度大于8时 会转换成红黑树
每个位置是16 比如线程1迁移 0-15 线程2 迁移 16-31

    public V put(K key, V value) {
        return putVal(key, value, false);
    }
/** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //根据hash code计算hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        //自旋
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //如果 node是空 则进行初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //通过(n-1)&hash来计算当前key在table中的下标位置 如果没有值则将当前 node添加进去
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //正在扩容的情况下 多个线程协助扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
               
                V oldVal = null;
                //对当前数组位置的节点加锁
                synchronized (f) {
                    //判断当前节点是链表 还是红黑树
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            //从链表头节点开始遍历
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //如果存在相同的key 则修改 key对应的value
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                //构建node 然后加入链表中如果 根据key的hash值得到的数组下标已经有元素并且是链表元素则添加到链表的尾部
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                       //如果是红黑树
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

初始化

izeCtl =-1 表示当前线程抢占到初始化数组的资格正在初始化数组
sizeCtl = -N 用sizeCtl 值的二进制低16位来记录当前参与扩容的线程数量
sizeCtl = 0 表示数组未初始化 并且构造方法中没有指定长度
sizeCtl >0 如果数组已经初始化 name sizeCtl 的阈值 = 初始容量*0.75 如果未初始化则表示数组的初始容量

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        //当初始化完成退出
        while ((tab = table) == null || tab.length == 0) {
            //判断是否有其他线程在进行初始化
            if ((sc = sizeCtl) < 0)
                //把自己进入就绪状态 释放cpu的执行权
                Thread.yield(); // lost initialization race; just spin
           // cas 操作去抢占锁 确保只有一个线程能抢占到锁 
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    //初始化
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        //初始化 赋值给CHM中的table
                        table = tab = nt;
                        //计算下次扩容阈值
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

转换成红黑树

当数组长度小于等于64 并且链表长度大于等于8时 优先对数组进行扩容
当数组长度大于64 并且链表长度大于8时 会转换成红黑树

    if (binCount != 0) {
                     binCount>=8
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            //如果小于64则使用扩容解决
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                 //扩容为原来的二倍,调整某一个桶中元素过多的问题(超出了8个))
        //会触发某些桶中的元素重新分配,避免在一个桶中有太多的元素影响访问效率
                tryPresize(n << 1);
              //桶中存在结点,并且此结点的hash值大于0,调整红黑树的结构
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                    //锁住节点,把元素添加到树中
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }
private final void tryPresize(int size) {
         //判断扩容目标大小 如果大小为 MAXIMUM_CAPACITY的一半则直接扩容大小为 MAXIMUM_CAPACITY
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
      //否则通过      tableSizeFor计算当前size的最小幂方 如果当前size不等于2幂方 通过 tableSizeFor调整离size最近的幂方值
     tableSizeFor(size + (size >>> 1) + 1)
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            //判断是否初始化过 如果没有初始化则初始化
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            //c <= sc 表示当前已经有其他线程在进行扩容 不需要在进行扩容
            //n >= MAXIMUM_CAPACITY 已到达最大值没法扩容
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            else if (tab == table) {
                //得到二进制的数据
                //高16位 表示扩容标记 由于每次扩容时 n的值都不同 因此能保证每次扩容时这个标记的唯一性
               // 低16位表示扩容的线程数量    
                int rs = resizeStamp(n);
                   //当前已经有其他线程在执行扩容 
                if (sc < 0) {
                    Node<K,V>[] nt;
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                   // 并且当前线程可以协助扩容
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                //当前没有其他线程进行扩容 进行首次扩容
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
            }
        }
    }

并发扩容机制

在这里插入图片描述

第一次进来扩容的线程会创建出一个新表。长度为原来的2倍。
迁移元素从后往前(索引从大到小)。
迁移完成的桶在当前桶位置放一个ForwardIngNode类型的节点,表示该桶迁移完成
迁移时通过hash&n(原长度)就是判断高位来判断在新链表中的索引位置,跟HashMap扩容原理一样。
低位链表(树)存储到新表的原索引位置
高位链表(树)存储到新表的原索引 + n(原数组长度)的位置
迁移元素时会使用synchronized锁定当前桶位,锁对象就是当前桶位的头结点,这是分段锁的思想。
迁移时,会根据原桶中的节点创建一个新的节点(除了lastRun机制的节点外)。
最后一个扩容的线程在退出时会重新扫描原表判断是否有遗漏的桶没有迁移节点,然后将nextTable赋值给table,然后将nextTable置为NULL,将sizeCtl设置为新数组长度的3/4即扩容阈值。
当存在多个线程并发扩容及数据迁移时 默认情况下会给每个线程分配一个区间 默认是16 每个线程负责自己区间内的数据迁移工作
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        //让每个cpu 处理的区间相同  默认为16 
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        // nextTab == null = true
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                //创建了一个是原来长度2倍的Node数组
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                //将新构建的赋值给 nextTab
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            //nextTable = 新构建的nextTab
            nextTable = nextTab;
            //赋值下标
            transferIndex = n;
        }
        int nextn = nextTab.length;
        //被迁移的node
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            //判断是否还有待处理的数据迁移工作
            while (advance) {
                int nextIndex, nextBound;
                //判断区间是否分配完成
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                  //进行分配工作
               // 比如一个线程分配到了 0-15这个区间
               //  第二个线程就是16-31这个区间  因为默认是16   
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
         
            //如果数据迁移工作完成
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    //完成之后赋值 table
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                //如果还未完成 说明还有其他线程正在执行中 协助扩容还未完成
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                //开始迁移
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        //表示当前节点为普通节点 按照链表或者普通节点的方式进行扩容
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        //表示当前节点为红黑树、
                        //当红黑树节点小于6 会转换成链表
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            //lc 低位链  hc 高位链
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                // == 0 表示需要迁移的数据
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                //不需要迁移的数据
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

高低位迁移

在这里插入图片描述

低位表示不需要迁移的元素
高位表示需要迁移的元素

分断锁设计提高统计元素数量的性能

当线程竞争不激烈时 直接使用baseCount+1来增加元素的个数
当线程竞争激烈时 构建一个 CountCell数组 默认长度为2 通过随机算法选择一个 CounterCell针对该CounterCell中的value进行保存

private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        if ((as = counterCells) != null ||
            //对 baseCount进行累加 当有竞争的时候会返回 false
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            // as == null 说明CounterCell 数组还未初始化
            // (m = as.length - 1) < 0  说明CounterCell 数组还未初始化
            //(a = as[ThreadLocalRandom.getProbe() & m]) == null  说明CounterCell已经创建 但是通过探针hash定位发现数组中还没有实例 说明这个数组中还存在没有CounterCell实例对象的情况
            //U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)) 说明当前 CounterCell数组每个位置都有一个CounterCell实例对象 直接通过 cas操作针对上一个步骤获得的 CounterCell的value值进行累加 如果失败则说明存在竞争
            
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                //计算size
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            //CounterCell数组的个数和
            s = sumCount();
        }
     //是否需要扩容
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }
private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();      // force initialization
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
            //表示数组已经初始化完成
            if ((as = counterCells) != null && (n = as.length) > 0) {
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) {    
                       // Try to attach new Cell
                        //创建 CounterCell对象
                        CounterCell r = new CounterCell(x); // Optimistic create
                   
                        if (cellsBusy == 0 &&
                            //cas 当前线程独占
                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            boolean created = false;
                            try {               // Recheck under lock
                                CounterCell[] rs; int m, j;
                                if ((rs = counterCells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    //将新构建的元素个数 保存到 rs[j]
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
                else if (counterCells != as || n >= NCPU)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        //竞争激烈 数组的扩容
                        if (counterCells == as) {// Expand table unless stale
                            //在原有的基础上扩容二倍  
                            CounterCell[] rs = new CounterCell[n << 1];
                            for (int i = 0; i < n; ++i)
                                //然后在进行数据迁移 
                                rs[i] = as[i];
                            //把扩容后的对象赋值给 counterCells
                            counterCells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                h = ThreadLocalRandom.advanceProbe(h);
            }
                // 抢占到锁 将 cellsBusy 修改成1 表示独占状态
            else if (cellsBusy == 0 && counterCells == as &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                boolean init = false;
                try {                           // Initialize table
                    if (counterCells == as) {
                        //创建初始化为2的 CounterCell数组
                        CounterCell[] rs = new CounterCell[2];
                        //把增加的元素个数x 保存到  rs[h & 1]的位置
                        rs[h & 1] = new CounterCell(x);
                        //把 rs赋值给全局对象 counterCells
                        counterCells = rs;
                        init = true;
                    }
                } finally {
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            //对数组指定位置的元素进行累加
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }
  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-04-22 19:01:27  更:2022-04-22 19:05:31 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 7:22:56-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码