Java實(shí)現(xiàn)跳躍表的示例詳解
跳表全稱叫做跳躍表,簡稱跳表,是一個(gè)隨機(jī)化的數(shù)據(jù)結(jié)構(gòu),實(shí)質(zhì)就是一種可以進(jìn)行二分查找的有序鏈表。跳表在原有的有序列表上面增加多級(jí)索引,通過索引來實(shí)現(xiàn)快速查找。跳表不僅能提高搜索性能,同時(shí)也提高插入和刪除的性能,redis中的有序集合set就是用跳表實(shí)現(xiàn)的,面試時(shí)候也經(jīng)常會(huì)問。
這里我們?cè)紨?shù)據(jù)個(gè)數(shù)n=10,以間隔k=2建立索引,則第一層索引10/2=5個(gè),第二層⌈10/2^2⌉=3個(gè),第三層⌈10/2^3⌉=2個(gè),第四層⌈10/2^4⌉=1個(gè)。根據(jù)上圖我們來分析一下,跳表的結(jié)構(gòu)是一棵樹(除原始數(shù)據(jù)層外),樹的左指針指向?qū)?yīng)的下一層鏈表的節(jié)點(diǎn),右指針指向當(dāng)前鏈表的下一個(gè)節(jié)點(diǎn),且樹高為log(n),對(duì)于每一層需要比較的次數(shù)最多為k,則時(shí)間復(fù)雜度為O(k*log(n)),k為常數(shù)項(xiàng),所以跳表查詢時(shí)間復(fù)雜度為O(log(n))。因?yàn)樾枰~外的空間存儲(chǔ)索引,是典型的以空間換時(shí)間,空間復(fù)雜度為O(n)。
接下來我們自己實(shí)現(xiàn)一個(gè)跳表:
節(jié)點(diǎn)數(shù)據(jù)結(jié)構(gòu)定義:根據(jù)跳表結(jié)構(gòu),節(jié)點(diǎn)首先需要一個(gè)value存儲(chǔ)當(dāng)前節(jié)點(diǎn)值,需要一個(gè)next指針指向同一層的下一個(gè)節(jié)點(diǎn),需要一個(gè)nodeValue指針指向下一層對(duì)應(yīng)節(jié)點(diǎn),但是這里為了插入刪除方便,引入了一個(gè)prev指針,指向同一層的上一個(gè)節(jié)點(diǎn)。
class Node { //當(dāng)前節(jié)點(diǎn)值 private Integer value; //當(dāng)前節(jié)點(diǎn)所屬鏈表下一個(gè)節(jié)點(diǎn) private Node next; //當(dāng)前節(jié)點(diǎn)所屬鏈表上一個(gè)節(jié)點(diǎn) private Node prev; //當(dāng)前節(jié)點(diǎn)指向的另一個(gè)索引鏈表/原始值鏈表節(jié)點(diǎn) private Node nodeValue; Node(Integer value) { this.value = value; } }
初始化一個(gè)跳表:跳表的建立需要在數(shù)據(jù)有序的基礎(chǔ)上,然后從下往上在下一層的基礎(chǔ)上,間隔k生成當(dāng)前層的節(jié)點(diǎn),新生成的節(jié)點(diǎn)需要與當(dāng)前層上一個(gè)節(jié)點(diǎn)連接起來,并且指向生成它的下一層節(jié)點(diǎn)。
/** * 原始數(shù)據(jù)鏈表 */ private Node head ; /** * 最終的跳表結(jié)構(gòu):保存索引鏈表及原始鏈表 */ private List<Node> indexList; /** * 跳表層數(shù) */ private int level; /** * 初始化 */ public void init() { //帶頭節(jié)點(diǎn)的鏈表,便于操作 head = new Node(-1); head.next = head; indexList = new ArrayList<>(); level = 0; } /** * 初始化跳表 * @param k 間隔 * @param nums 原始數(shù)據(jù)(已排序) */ public void init(int k, int[] nums) { //初始化數(shù)據(jù)鏈表 Node temp = head; for (int num : nums) { Node cur = new Node(num); cur.prev = temp; temp.next = cur; temp = temp.next; } //新節(jié)點(diǎn)保存(最底層) indexList.add(head); //循環(huán)生成索引結(jié)構(gòu),結(jié)束條件,當(dāng)層僅一個(gè)元素 temp = head.next; while (true) { //當(dāng)前鏈表第幾個(gè)元素 int i = 0; //生成另一條鏈表長度 int size = 0; Node indexNode = new Node(-1); indexNode.next = indexNode; Node indexNodeTemp = indexNode; while (null != temp) { //間隔k生成節(jié)點(diǎn) if (i % k == 0) { Node curNode = new Node(temp.value); curNode.nodeValue = temp; curNode.prev = indexNodeTemp; indexNodeTemp.next = curNode; indexNodeTemp = indexNodeTemp.next; ++ size; } ++ i; temp = temp.next; } indexList.add(indexNode); temp = indexNode.next; //當(dāng)生成的索引鏈表僅1時(shí)不需要再繼續(xù)生成 if (size == 1) { break; } } level = indexList.size(); }
從跳表中查找元素:從最頂層索引鏈表開始查找,找到第一個(gè)大于當(dāng)前節(jié)點(diǎn)的元素,則需要查找的元素在當(dāng)前節(jié)點(diǎn)與之前節(jié)點(diǎn)之間,則從當(dāng)前節(jié)點(diǎn)的上一個(gè)節(jié)點(diǎn)prev往下nodevalue繼續(xù)進(jìn)行查找,直到當(dāng)前節(jié)點(diǎn)值與查找值相等,則直接返回當(dāng)前節(jié)點(diǎn),返回的節(jié)點(diǎn)可能是索引節(jié)點(diǎn),也可能是原始數(shù)據(jù)節(jié)點(diǎn),如果需要找到原始數(shù)據(jù)節(jié)點(diǎn),則通過nodeValue繼續(xù)往下找。
/** * 是否存在num * @param num * @return */ public boolean hasNum(int num) { Node result = this.findNum(num); return null != result; } /** * 查找num(返回的可能是索引,也可能是原始數(shù)據(jù),根據(jù)nodeValue可以判斷,也可以找到原始數(shù)據(jù)) * @param num */ public Node findNum(int num) { //跳表結(jié)構(gòu)indexList是數(shù)據(jù)-》第一層索引-》第二層索引-》。。。。 //1.直接匹配到 //2.找到第一個(gè)大于當(dāng)前元素的數(shù),找前一個(gè) Node node = indexList.get(indexList.size() - 1).next; Node last = null; while (null != node) { if (node.value == num) { //已經(jīng)找到元素 return node; } if (node.value > num) { if (null == last) { //比最小值還小 return null; } //找到了第一個(gè)大于num的索引node //到下一層去繼續(xù)找 node = last.nodeValue; last = null; continue; } last = node; node = null != node.next ? node.next : node.nodeValue; } return null; }
刪除節(jié)點(diǎn):首先通過上面的查找方法找到目標(biāo)節(jié)點(diǎn),如果目標(biāo)節(jié)點(diǎn)是索引值,則需要從當(dāng)前索引層,層層往下刪除包括原始數(shù)據(jù)鏈表,如果是原始數(shù)據(jù)值,則直接刪除,暫不調(diào)整。
/** * 構(gòu)建索引時(shí):自底向上逐層構(gòu)建,如果索引需要?jiǎng)h除(當(dāng)兩個(gè)索引之間沒有任何數(shù)據(jù)時(shí)候,刪除) * @param num * @return */ public boolean remove(int num) { Node node = this.findNum(num); if (null == node) { //不需要移除 return false; } if (null == node.nodeValue) { //數(shù)據(jù)鏈表,可以直接移除 //是否最后一個(gè)節(jié)點(diǎn) if (null == node.next) { node.prev.next = null; return true; } node.next.prev = node.prev; node.prev.next = node.next; return true; } //當(dāng)前在索引上,自上而下刪除索引及數(shù)據(jù) while (null != node) { Node cur = node.nodeValue; if (null == node.next) { node.prev.next = null; } else { node.next.prev = node.prev; node.prev.next = node.next; } node = cur; } return true; }
新增節(jié)點(diǎn):新增節(jié)點(diǎn)時(shí)候,如果不對(duì)索引進(jìn)行調(diào)整,極端情況下,每次新增的節(jié)點(diǎn)都在之前第一層兩個(gè)節(jié)點(diǎn)之間,當(dāng)這之間的鏈表越變?cè)介L,時(shí)間復(fù)雜度直接退化為O(n),所以需要同時(shí)新增索引,維持跳表的高效性。但是我們?nèi)绾涡略?,有一個(gè)方法就是,在新增節(jié)點(diǎn)時(shí),隨機(jī)選擇k,即第k級(jí)索引,從1~k新增索引。
/** * 首先需要查找插入位置,如果比最小的還小,直接在前面插入 * 否則需要從最頂級(jí)一直查找到數(shù)據(jù)鏈表,找到插入位置,插入,在查找的過程中,就可以開始插入索引節(jié)點(diǎn), * 從上往下進(jìn)行插入 * @param num */ public void add(int num) { int k = this.generatorLevelK(); //尋找插入點(diǎn)的過程和查找過程基本一致 //頂級(jí)索引鏈表 Node node = indexList.get(indexList.size() - 1).next; int index = 1; while (null != node) { //找到第一個(gè)node.value >= num的元素,在前面插入 if (node.value >= num) { //已經(jīng)找到,前插 if (index >= k) { Node newNode = new Node(num); Node temp = node.prev; newNode.next = temp.next; temp.next.prev = newNode; newNode.prev = temp; temp.next = newNode; } //找的時(shí)候往后面找的,但是當(dāng)前已經(jīng)先于num了,下一次再往后面找,就出現(xiàn)問題 if (null == node.prev.prev) { //第一個(gè)節(jié)點(diǎn)就符合條件 node = node.nodeValue; continue; } node = node.prev.nodeValue; ++ index; continue; } //沒有找到,但是當(dāng)前已經(jīng)是鏈表最后一個(gè)元素了 if (null == node.next) { if (index >= k) { Node newNode = new Node(num); newNode.prev = node; node.next = newNode; } if (null == node.prev.prev) { //第一個(gè)節(jié)點(diǎn)就符合條件 node = node.nodeValue; continue; } node = node.prev.nodeValue; ++ index; continue; } node = node.next; } } private int generatorLevelK() { Random random = new Random(); return random.nextInt(level); }
至此,我們實(shí)現(xiàn)了一個(gè)跳表的定義,初始化,查找,節(jié)點(diǎn)新增與刪除。
到此這篇關(guān)于Java實(shí)現(xiàn)跳躍表的示例詳解的文章就介紹到這了,更多相關(guān)Java跳躍表內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決mybatis where-if中if不能識(shí)別大寫AND,OR的問題
這篇文章主要介紹了解決mybatis where-if中if不能識(shí)別大寫AND,OR的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-02-02Java中l(wèi)ist根據(jù)id獲取對(duì)象的幾種方式
這篇文章主要給大家介紹了關(guān)于Java中l(wèi)ist根據(jù)id獲取對(duì)象的幾種方式,文中通過實(shí)例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用java具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07SpringBoot在生產(chǎn)快速禁用Swagger2的方法步驟
這篇文章主要介紹了SpringBoot在生產(chǎn)快速禁用Swagger2的方法步驟,使用注解關(guān)閉Swagger2,避免接口重復(fù)暴露,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2018-12-12MyBatis CodeHelperPro激活方法詳細(xì)教程
MyBatisCodeHelper-Pro是IDEA下的一個(gè)插件,功能類似mybatis plugin,今天小編給大家分享MyBatis CodeHelperPro激活方法,需要的朋友跟隨小編一起看看吧2021-07-07