新闻详情

新闻详情

首页 / 资讯中心 / 详情

Java数据结构十篇串联:从排序、链表到HashMap底层原理

发布时间:2026/9/30 11:38:54来源:尧图网络
Java数据结构十篇串联:从排序、链表到HashMap底层原理
写到这里Java数据结构系列已经到了第十篇。后台经常有读者问我前面九篇的笔记都做完了面试题也刷了一部分可为什么一碰到“讲一下HashMap底层原理”这种问题还是会卡壳我的答案很直接你缺的不是知识点而是把知识点串起来的那条线。这一篇我不打算继续往前讲新结构而是回头把排序、链表、双端队列、集合源码这些高频考点拧在一起讲讲它们背后的设计逻辑顺便分享几个我自己写代码时踩过的坑。不管你是刚把前面的内容学完还是中途跳进来想查漏补缺这篇文章都可以当作一份阶段性的串联笔记也可以看作是“Java面试八股文”里数据结构部分的破题思路。1. 为什么写到第十篇反而要回头整理知识脉络1.1 前九篇到底串了一条什么线如果你是从第一篇跟到现在应该已经接触过线性表、栈与队列、二叉树、图、哈希表、堆以及查找和排序这些主题。单看每一个结构都觉得自己懂了但把它们放在一起很容易变成一团浆糊。我之前带过一个刚入行的同事他能把红黑树的左旋右旋背得滚瓜烂熟可问他“为什么TreeMap要用红黑树而不是AVL树”他愣了一下说“因为面试题这么写的”。这里的关键在于数据结构本质上解决的是同一个问题如何组织数据让增删改查在时间和空间上达到平衡。数组是连续内存随机访问快但插入删除要搬移元素链表靠指针串联插入删除快但要遍历才能找到目标树和哈希表则是在这两种极端之间找折中方案。学到这里你应该形成一条主线线性结构解决“排队”问题树结构解决“层次关系”问题图解决“多对多关系”问题哈希表解决“快速定位”问题。如果这条主线没建立起来后面看任何源码都会觉得是一堆孤立的知识点。我个人的建议是在第十篇这个节点上花一个晚上把前九篇的目录翻出来用一张大白纸画出这些结构之间的关系。比如栈可以用数组实现也可以用链表实现二叉树在极端情况下会退化成链表HashMap在树化之后又用到了红黑树。你会发现这些结构不是彼此孤立的A结构的缺点恰好是B结构的优点它们互相补充。1.2 学完九篇之后最常见的三个误区第一个误区只背代码不画图。链表反转这道题很多人把代码背得滚瓜烂熟可面试官让他讲一遍指针变化过程他反而说不清楚。数据结构是空间结构眼睛看着图去理解一轮操作和只在IDE里跑通一遍代码效果差别非常大。我自己的做法是每学一个新结构就在纸上手动模拟一次插入、删除和查找过程哪怕只是画简笔画一样的方框和箭头也比干看代码有效得多。第二个误区知识点之间不建立联系。学排序的时候只记住了快排时间复杂度是O(n log n)却不去想为什么JDK底层针对基本类型和对象用了两套不同的排序算法学HashMap的时候只记住“链表转红黑树”这几个字却不理解什么条件下才会触发转换更不理解为什么数组长度不够时优先扩容而不是直接树化。这些“为什么”恰恰是面试官想听的也是你真的会用和假装会用的分水岭。第三个误区只看不练。数据结构是手艺活就像骑自行车看再多教程都不如自己上手摔一次。每学完一个结构至少要自己独立写出它的关键操作栈的入栈出栈、树的层序遍历、堆的上浮下沉、图的广度优先搜索。写不出来的地方就是理解断掉的地方把这些断点记下来远比一路向前滑更有价值。2. 排序算法专题冒泡排序背后的“比较与交换”比背模板重要2.1 冒泡排序的完整推导与优化点先来聊聊冒泡排序。很多人觉得它简单不值一提但我面试过不少候选人能把冒泡排序的完整推导过程讲清楚的并不多。冒泡排序的核心不是“把最大的数冒到末尾”这个结论而是每一轮都做一次局部比较和交换把当前未排序范围内的最大值送到最后面。public static void bubbleSort(int[] arr) { if (arr null) return; for (int i 0; i arr.length - 1; i) { boolean swapped false; for (int j 0; j arr.length - 1 - i; j) { if (arr[j] arr[j 1]) { int tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; swapped true; } } if (!swapped) { break; } } }内层循环的边界为什么是arr.length - 1 - i因为经过i轮排序后数组末尾的i个元素已经是全局最大的i个数它们的位置固定下来不需要再参与比较。swapped标志是常见的优化点如果某一轮从头到尾一次交换都没有发生说明数组已经有序可以直接结束。这个“提前退出”的优化让冒泡排序在最好情况下的时间复杂度变成O(n)。我实际写业务代码时很少直接用冒泡排序但用“相邻交换”这个思路解决一些数组局部有序的问题反而常常用到。比如需要把一串相邻逆序的数纠正过来冒泡的思考方式会帮你快速定位到需要交换的位置。学习冒泡排序更重要的是体会“一轮比较确定一个元素的最终位置”这个思想因为后面学选择排序、堆排序你会发现都有类似的味道。2.2 快速排序与归并排序分治思想在Java中的落地排序里最值得花时间练的其实是快速排序和归并排序它们都是典型的分治算法。快速排序的核心是分区选一个基准值把数组分成左边小、右边大两部分然后递归处理左右两侧。Java的Arrays.sort对基本类型数组用的是一种双轴快排对对象数组用的却是TimSort一种归并排序的变体原因很值得琢磨基本类型排序不需要稳定性快排常数小、速度更快对象排序往往需要稳定比如你先把学生按姓名排好再按年龄排一次如果第二次排序不稳定相同年龄的学生姓名顺序就可能被打乱。归并排序的思路则是先拆到底再两两合并。它的时间复杂度稳定在O(n log n)但需要额外O(n)的辅助空间。有一点很有意思链表排序时归并排序往往比快排更合适因为链表不需要随机访问也能以O(1)代价合并两颗有序链表快排在链表的随机访问上反而吃亏。我在实际中见过一个案例业务里有几十万条订单数据需要按金额排序后分页展示开始直接用数据库排序返回后来数据量上来之后接口变慢改成在Java内存里用归并排序的思想做多路合并反而快了不少。学排序不要只满足于背出时间复杂度和空间复杂度要能说出“为什么这个场景应该用这种而不是那种”。2.3 排序稳定性对实际业务的影响面试里经常问“稳定性”这个概念。很多人能背出“稳定的排序算法冒泡、插入、归并不稳定的选择、快排、堆排”但并不知道稳定性到底影响了什么。这里说一个业务例子有一个商品列表先按销量排序再按上架时间排序。如果第二次排序用的算法不稳定那么两个上架时间相同的商品它们的销量相对顺序就可能被换掉用户看到的列表就会忽前忽后体验很怪。这就是为什么Java的Collections.sort内部对对象使用稳定排序因为对象排序往往意味着多重排序条件的叠加。你在写自定义比较器时也应该记住这点如果同一个排序条件可以被多次施加尽量选择稳定的排序算法或者把次要条件写进同一个比较器里。实际上最稳妥的做法是不要把“两次排序”当成分开的操作而是用一个Comparator把多级条件一次排好主字段相同再比较次字段。3. 链表这个数据结构值得你多花一周时间反复练3.1 反转链表的两种写法链表在面试里出现频率极高尤其是反转链表、环形链表、合并有序链表这几道题。拿反转链表来说迭代法的代码很简洁但每一行都是有含义的public ListNode reverseList(ListNode head) { ListNode prev null; ListNode curr head; while (curr ! null) { ListNode next curr.next; curr.next prev; prev curr; curr next; } return prev; }这里next curr.next要先保存当前节点的下一个节点否则一旦把curr.next指向prev链表的后半段就丢了。prev和curr两个指针一前一后往前走每轮循环把当前节点的指针掉转方向。整个过程很像排队时集体向后转每个人先记住身后的人然后转身面向原来身后的人。递归写法更短但空间复杂度是O(n)因为递归深度就是链表长度。现在大部分面试官看到递归解法后都会追问一句“递归和迭代的区别是什么分别有什么代价”你需要能说出来递归代码简洁但占用栈空间链表很长时可能栈溢出迭代虽然多写几行但空间是O(1)。实际开发里我几乎只用迭代法写链表反转原因就是稳定、可控。3.2 快慢指针环检测与寻找中间节点环形链表检测是另一个高频题标准的解法是快慢指针public boolean hasCycle(ListNode head) { ListNode slow head; ListNode fast head; while (fast ! null fast.next ! null) { slow slow.next; fast fast.next.next; if (slow fast) { return true; } } return false; }快指针每次走两步慢指针每次走一步如果链表有环两个人必然会在某个节点相遇。这里有个细节经常被忽略为什么快指针走两步就够了走三步行不行两步的优势在于快指针和慢指针的相对速度差是1这样每轮循环慢指针相对快指针的“距离”减少1不会跳过去。如果步长差大于1有可能在环里的某个位置反复跳过导致无法准确判断相遇条件并且代码边界条件会变得更复杂。所以面试时如果被问到能解释“相对速度”这层原因印象分会高不少。快慢指针还能用来找链表中间节点快指针走到末尾时慢指针正好在中间。这个技巧在“对半分割链表”的场景中经常出现比如归并排序链表的第一步。写这类题时我会在纸上画一个环手动走两轮看看slow和fast分别跑到哪个节点这种方式比直接看代码直观得多。3.3 Java中LinkedList的真实使用场景与性能陷阱LinkedList是Java自带的双向链表容器但它在大众认知里的口碑其实不太好因为很多人拿它当ArrayList用随机访问的时候性能惨不忍睹。LinkedList的get(int index)要沿着链表从头走到目标位置复杂度是O(n)而ArrayList的随机访问是O(1)。所以如果你的场景需要频繁按下标取元素老老实实用ArrayList。那LinkedList到底适合什么场景答案是需要在头部和尾部频繁插入删除或者在链表中间已知位置附近进行插入删除。比如实现一个消息队列的缓冲区频繁从尾部offer、从头部pollLinkedList的表现就很好。还要注意一个细节LinkedList允许存null但如果你把它当成队列用通常不建议放空值否则取值时要多判一次空容易埋坑。反观ArrayDeque它不允许null这反而是一种约束能帮你在早期发现逻辑问题。4. 双端队列一个常被忽略却非常实用的容器4.1 双端队列解决什么问题上热搜词里出现了“数据结构 双端队列”这是个好信号说明越来越多人注意到这个概念。队列的本质是“一端进、一端出”栈的本质是“同一端进和出”而双端队列把两端的操作都开放了既能从头部加也能从尾部加既能从头部取也能从尾部取。Java里的Deque接口就是双端队列的标准抽象它的实现类主要有ArrayDeque和LinkedList。为什么需要双端队列因为很多实际问题里数据并不是严格只从一个方向进出。比如浏览器的前进后退你用两个栈就能实现再比如某些窗口滑动问题你需要的不是简单排队而是既能从尾部加入新元素又能从头部淘汰过期元素。双端队列把这些场景统一了你可以更自由地控制数据流动方向。我记得自己第一次真正理解Deque的价值是在实现一个“撤销重做”功能时。原本我维护两个Stack后来发现每步操作都要频繁在两个栈之间搬数据而ArrayDeque能提供更简洁的双端操作代码结构一下清晰不少。这就是双端队列的实际意义它不是一个炫技的数据结构而是能实实在在简化某些逻辑的工具。4.2 ArrayDeque与LinkedList的差异在哪里ArrayDeque和LinkedList虽然都实现了Deque但底子完全不同。ArrayDeque底层是一个循环数组LinkedList底层是双向链表这个差异带来了一系列具体表现对比维度ArrayDequeLinkedList底层结构循环数组双向链表随机访问不支持只能遍历不支持只能遍历头部/尾部插入删除O(1)但偶尔扩容O(1)无扩容内存布局连续数组更紧凑分散节点有额外指针开销是否允许null不允许允许作为栈/队列使用推荐也可以但指针开销更大使用ArrayDeque当栈的一个额外好处是它不像Stack那样在每一句操作上都加锁。Stack继承自Vector所有方法都带synchronized单线程环境下反而徒增开销。Java官方文档也建议需要栈语义时优先使用ArrayDeque。我之前一直用LinkedList当队列直到有一次在内存分析里发现它每个节点都要维护前后指针导致对象数量比ArrayList翻了几倍那次之后我就养成了习惯用队列首选ArrayDeque只在确实需要链表特性时才选LinkedList。4.3 用ArrayDeque实现滑动窗口最大值双端队列最经典的实战场景我觉得是求滑动窗口的最大值。给你一个数组和一个窗口大小k窗口每次往右移动一格需要输出窗口内的最大值。直观做法是每到一个新窗口都扫描一遍复杂度O(nk)数据量大时非常慢。用双端队列可以做到O(n)队列里存下标始终保持从队首到队尾的下标对应的值单调递减这样队首永远就是窗口最大值。public static int[] maxSlidingWindow(int[] nums, int k) { if (nums null || nums.length 0) { return new int[0]; } int n nums.length; int[] result new int[n - k 1]; ArrayDequeInteger deque new ArrayDeque(); for (int i 0; i n; i) { while (!deque.isEmpty() nums[deque.peekLast()] nums[i]) { deque.pollLast(); } deque.offerLast(i); if (deque.peekFirst() i - k 1) { deque.pollFirst(); } if (i k - 1) { result[i - k 1] nums[deque.peekFirst()]; } } return result; }这段代码有三步值得细看。第一步新元素从尾部进入前先去掉所有比它小或等于它的旧元素的下标因为这些旧元素在窗口里永远不可能成为最大值第二步检查队首下标是否已经滑出当前窗口如果滑出了就移除第三步窗口满员后队首下标对应的值就是当前窗口最大值。整个过程每个元素最多入队出队一次所以总时间是O(n)。我第一次写这个题时犯过一个低级错误直接往队列里存值而不是存下标结果窗口滑出时根本不知道哪个值是否还属于当前窗口。后来才明白存下标的妙处在于能通过下标同时判断值的大小和是否过期。这是一种很值得积累的套路凡是需要“既要比较大小又要判断是否过期”的场景优先考虑存下标。5. JDK集合源码里藏着的数据结构答案5.1 HashMap的哈希冲突处理与扩容机制“HashMap底层原理”大概是Java面试里出场率最高的问题之一。从数据结构角度看JDK8的HashMap是“数组 链表 红黑树”的组合。put一个键值对时先对key的hashCode做一次扰动计算然后通过(n - 1) hash定位到数组下标。如果这个位置上没有元素直接放入如果有元素就沿着链表往后找如果找到相同的key就替换值如果没找到就新增节点。当某个桶位上的链表长度达到8并且整个数组长度大于等于64时这个链表会转换成红黑树目的是把查询时间从最坏的O(n)降为O(log n)。很多面试者只记住“链表长度大于8转红黑树”却忽略前置条件“数组长度不小于64”。如果数组长度还很小HashMap会优先扩容而不是转树因为扩容本身就是一种更快、更均匀的分散冲突手段。扩容发生在size超过threshold时threshold是容量乘以负载因子的结果。默认负载因子0.75容量16threshold就是12。为什么是0.75而不是1因为负载因子太高时哈希冲突概率明显上升链表过长查询退化负载因子太低时数组大量位置空闲浪费内存。0.75是时间和空间的折中这个值不是拍脑袋定的而是统计经验里一个比较合理的平衡点。5.2 fail-fast机制为什么遍历时不允许修改用迭代器遍历ArrayList时如果在另一个线程或同一循环里调用list.remove大概率会抛出ConcurrentModificationException。这个机制叫fail-fast它的实现并不复杂集合内部维护一个modCount字段每次结构性修改增、删、扩容都会让这个计数加1迭代器创建时会记录当时的expectedModCount每走一步检查这两个值是否一致不一致就立刻抛异常。为什么要这么做因为很多集合类并不保证遍历期间的并发修改安全。如果在迭代过程中直接修改了集合结构迭代器依赖的索引、链表指针、桶位都会变得不可靠继续遍历只会产生错误结果或死循环。fail-fast的作用就是尽早暴露问题而不是让错误静默发生。需要注意的是fail-fast机制并不是为多线程并发设计的它依赖modCount这个字段而字段的读写并不保证线程可见性所以不要指望它在高并发场景下一定帮你兜底。真正需要并发修改集合时应该选择CopyOnWriteArrayList或ConcurrentHashMap这类专为并发设计的容器。我遇到过不少同事在循环里想删元素第一反应是“用迭代器就可以了”但如果业务场景确实允许并发操作还是换成并发容器更稳妥。5.3 从集合源码反推数据结构原理看JDK源码是学数据结构一个很自然的深化方式。每学一个容器可以先猜它底层用了什么结构再去源码里验证。比如ArrayList底层就是一个Object[]数组扩容时机是当前容量不够时新容量大约变成旧容量的1.5倍也就是oldCapacity (oldCapacity 1)LinkedList底层就是上面说的双向链表TreeMap和TreeSet底层是红黑树所以它们的键是有序的PriorityQueue底层是一个二叉堆因此每次取出的是优先级最高的元素。当你把“容器”和“数据结构”对应起来之后很多面试题就不再是死记硬背。“ArrayList和LinkedList的区别”本质上是“数组和链表的区别”“HashMap和Hashtable的区别”本质上是“线程安全策略、初始容量、扩容策略的组合差异”。我自己是这么做的每学一个新容器先不看文档直接打开源码看它的字段推断结构再跑一段测试验证。这种方法看起来费时间但对理解深度的提升是立竿见影的。6. 从入门到精通的“最后一公里”把知识串成体系6.1 把八股文变成自己的话很多人害怕面试里的“八股文”。我的看法是八股本身不是问题问题在于只背结论不推导。比如问到“什么是红黑树”如果你只能说出“一种自平衡二叉查找树”那确实很干如果你能说出“它用颜色标记节点来约束路径上的黑色节点数量从而保证最长路径不会超过最短路径的两倍避免二叉查找树在极端插入顺序下退化成链表”面试官就能判断你是真懂。具体可以这样练习针对每个高频知识点写一段不超过五句话的自我解释讲给身边同事听。比如HashMap我就经常用一句话记忆“用哈希表做快速定位用链表解决冲突冲突多了用红黑树保持查询效率容量不够就翻倍扩容。”这句话虽然简化了很多细节但已经把最关键的信息都覆盖了。接下来再对着这句话逐层展开哈希表怎么定位、链表怎么插入、红黑树怎么转换、扩容怎么搬移。展开的过程就是知识内化的过程。6.2 一道综合练习手写LRU缓存LRU最近最少使用缓存是一个非常经典的综合题正好能把HashMap和双向链表结合到一块。思路是用一个HashMapkey存缓存的键value指向双向链表中的节点双向链表按访问时间从新到旧排列。每次访问某个key就把对应节点移到链表头部需要淘汰时直接移除链表尾部的节点。这样查询和淘汰操作都能在O(1)时间内完成。面试里如果允许最简单的方式是直接用LinkedHashMap把构造参数accessOrder设为true同时重写removeEldestEntry方法来控制容量上限。但面试官通常希望你写出底层逻辑所以还得掌握手写HashMap 双向链表的版本。写的时候双向链表的节点要同时存key和value因为HashMap只记录了key到节点的映射淘汰尾部节点时你需要通过节点里的key删除HashMap中的对应项。这个细节很多人想不到恰恰是手写LRU最容易出错的地方。6.3 刻意练习与实操安排建议到了这个阶段我建议你用两周时间做一个专题收尾。第一周把前面九篇里的所有结构在纸上画一遍再手写一遍核心代码栈的数组实现、二叉树的先序中序后序遍历、堆的插入和删除、图的邻接表与深度优先搜索。第二周把你的核心代码和JDK源码对照看看自己的实现和官方实现差在哪里。比如你手写的栈扩容是不是也是两倍扩容你手写的二叉树删除有没有考虑被删除节点有两个子节点的情况。这种对照练习是在“会用”和“理解原理”之间架桥。还有一个方法我觉得特别有用找一个周末的下午给自己一个题目比如“设计一个支持过期时间的缓存”然后不查资料限时两小时白纸上从数据结构选型到代码落地完整走一遍。这种模拟真实开发的练习比单纯刷几十道题更能检验水平。最后聊一点个人体会。数据结构的“精”从来不是靠囤资料囤出来的。我见过有些朋友收藏了十几个教程链接电脑里放了一堆PDF却连链表反转都没能白板写出来。相反一个能把数组、链表、哈希表三者区别讲清楚、能在纸上画出二叉树三种遍历顺序的人哪怕代码写得慢一点我也觉得他是真正学到了东西。如果你也正好走到这个系列的第十篇不妨就按上面这个节奏来一遍把前面所有的知识就像串珠子一样穿成一条项链。你会发现后面再遇到任何面试题或是工程问题心里都会更有底。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

SpringBoot动漫分享系统实战:从数据库设计到Docker部署 2026/9/30 12:27:18

SpringBoot动漫分享系统实战:从数据库设计到Docker部署

1. 项目背景与定位:为什么要做“动漫分享系统”说实话,每年毕业季我都会看到大量“基于SpringBoot的XX管理系统”选题,动漫分享系统算其中比较有代表性的一个。它不是简单的CRUD堆砌,而是把用户、内容、评论、收藏、文件上传、视频…

阅读更多 →
Agent Skills安全实践清单:DefaultAzureCredential认证、防密钥泄露与智能体安全配置 2026/9/30 12:27:04

Agent Skills安全实践清单:DefaultAzureCredential认证、防密钥泄露与智能体安全配置

Agent Skills安全实践清单:DefaultAzureCredential认证、防密钥泄露与智能体安全配置 【免费下载链接】skills Skills, MCP servers, Custom Agents, Agents.md for SDKs to ground Coding Agents 项目地址: https://gitcode.com/gh_mirrors/agent/skills Ag…

阅读更多 →
FDE落地:FDE不断落地,82亿美元,AMD全股票收购李飞飞的World Labs 2026/9/30 12:26:57

FDE落地:FDE不断落地,82亿美元,AMD全股票收购李飞飞的World Labs

82亿美元,AMD全股票收购李飞飞的World Labs。 从2024年初创办到2026年中签署收购协议,空间智能公司World Labs练习时长一坤年。 交割完成后,李飞飞将加入AMD担任执行副总裁兼首席科学家,直接向董事长兼CEO苏姿丰汇报。 联合创始人…

阅读更多 →
Unity粒子系统底层原理与URP跨平台优化指南 2026/9/30 12:26:50

Unity粒子系统底层原理与URP跨平台优化指南

1. 为什么“粒子效果”不是特效的终点,而是你理解Unity渲染管线的起点“【实现100个unity特效之7】unity 3d实现各种粒子效果”——这个标题乍看是教程合集里平平无奇的一节,但如果你真把它当成“拖几个预设、调几个滑块就能交差”的任务,那接…

阅读更多 →
华为全栈智能数据中心解决方案:架构分层与落地实践指南 2026/9/30 12:26:50

华为全栈智能数据中心解决方案:架构分层与落地实践指南

简介:这份PDF文档聚焦华为全栈智能数据中心解决方案,面向金融、电信、政府等行业中负责数据中心规划、建设与运维的架构师、IT管理者及数字化转型决策者,帮助其理解如何借助全栈智能技术降低TCO、提升业务效率。资源包内仅含1个PDF文件&#…

阅读更多 →
字符串数组实战指南:从初始化到内存布局与分割查找 2026/9/30 12:26:50

字符串数组实战指南:从初始化到内存布局与分割查找

你说得对,上一篇把字符数组和字符串数组的基础概念过了一遍,评论区很多朋友说“看懂了,但是一上手写代码就被字符串搞到头大”。这期我不打算重复基础定义,直接把平时实际项目中遇到的高频问题拎出来讲:初始化那些看似…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉