跳錶全稱叫做跳躍表,簡稱跳錶,是一個隨機化的數據結構,實質就是一種可以進行二分查找的有序鏈表。跳錶在原有的有序列表上面增加多級索引,通過索引來實現快速查找。跳錶不僅能提高搜索性能,同時也提高插入和刪除的性能,redis中的有序集合set就是用跳錶實現的,面試時候也經常會問。
這裏我們原始數據個數n=10,以間隔k=2建立索引,則第一層索引10/2=5個,第二層⌈10/2^2⌉=3個,第三層⌈10/2^3⌉=2個,第四層⌈10/2^4⌉=1個。根據上圖我們來分析一下,跳錶的結構是一棵樹(除原始數據層外),樹的左指針指向對應的下一層鏈表的節點,右指針指向當前鏈表的下一個節點,且樹高爲log(n),對於每一層需要比較的次數最多爲k,則時間複雜度爲O(k*log(n)),k爲常數項,所以跳錶查詢時間複雜度爲O(log(n))。因爲需要額外的空間存儲索引,是典型的以空間換時間,空間複雜度爲O(n)。
接下來我們自己實現一個跳錶:
節點數據結構定義:根據跳錶結構,節點首先需要一個value存儲當前節點值,需要一個next指針指向同一層的下一個節點,需要一個nodeValue指針指向下一層對應節點,但是這裏爲了插入刪除方便,引入了一個prev指針,指向同一層的上一個節點。
class Node { //當前節點值 private Integer value; //當前節點所屬鏈表下一個節點 private Node next; //當前節點所屬鏈表上一個節點 private Node prev; //當前節點指向的另一個索引鏈表/原始值鏈表節點 private Node nodeValue; Node(Integer value) { this.value = value; } }
/** * 原始數據鏈表 */ private Node head ; /** * 最終的跳錶結構:保存索引鏈表及原始鏈表 */ private List<Node> indexList; /** * 跳錶層數 */ private int level; /** * 初始化 */ public void init() { //帶頭節點的鏈表,便於操作 head = new Node(-1); head.next = head; indexList = new ArrayList<>(); level = 0; } /** * 初始化跳錶 * @param k 間隔 * @param nums 原始數據(已排序) */ public void init(int k, int[] nums) { //初始化數據鏈表 Node temp = head; for (int num : nums) { Node cur = new Node(num); cur.prev = temp; temp.next = cur; temp = temp.next; } //新節點保存(最底層) indexList.add(head); //循環生成索引結構,結束條件,當層僅一個元素 temp = head.next; while (true) { //當前鏈表第幾個元素 int i = 0; //生成另一條鏈表長度 int size = 0; Node indexNode = new Node(-1); indexNode.next = indexNode; Node indexNodeTemp = indexNode; while (null != temp) { //間隔k生成節點 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; //當生成的索引鏈表僅1時不需要再繼續生成 if (size == 1) { break; } } level = indexList.size(); }
/** * 是否存在num * @param num * @return */ public boolean hasNum(int num) { Node result = this.findNum(num); return null != result; } /** * 查找num(返回的可能是索引,也可能是原始數據,根據nodeValue可以判斷,也可以找到原始數據) * @param num */ public Node findNum(int num) { //跳錶結構indexList是數據-》第一層索引-》第二層索引-》。。。。 //1.直接匹配到 //2.找到第一個大於當前元素的數,找前一個 Node node = indexList.get(indexList.size() - 1).next; Node last = null; while (null != node) { if (node.value == num) { //已經找到元素 return node; } if (node.value > num) { if (null == last) { //比最小值還小 return null; } //找到了第一個大於num的索引node //到下一層去繼續找 node = last.nodeValue; last = null; continue; } last = node; node = null != node.next ? node.next : node.nodeValue; } return null; }
/** * 構建索引時:自底向上逐層構建,如果索引需要刪除(當兩個索引之間沒有任何數據時候,刪除) * @param num * @return */ public boolean remove(int num) { Node node = this.findNum(num); if (null == node) { //不需要移除 return false; } if (null == node.nodeValue) { //數據鏈表,可以直接移除 //是否最後一個節點 if (null == node.next) { node.prev.next = null; return true; } node.next.prev = node.prev; node.prev.next = node.next; return true; } //當前在索引上,自上而下刪除索引及數據 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; }
/** * 首先需要查找插入位置,如果比最小的還小,直接在前面插入 * 否則需要從最頂級一直查找到數據鏈表,找到插入位置,插入,在查找的過程中,就可以開始插入索引節點, * 從上往下進行插入 * @param num */ public void add(int num) { int k = this.generatorLevelK(); //尋找插入點的過程和查找過程基本一致 //頂級索引鏈表 Node node = indexList.get(indexList.size() - 1).next; int index = 1; while (null != node) { //找到第一個node.value >= num的元素,在前面插入 if (node.value >= num) { //已經找到,前插 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; } //找的時候往後面找的,但是當前已經先於num了,下一次再往後面找,就出現問題 if (null == node.prev.prev) { //第一個節點就符合條件 node = node.nodeValue; continue; } node = node.prev.nodeValue; ++ index; continue; } //沒有找到,但是當前已經是鏈表最後一個元素了 if (null == node.next) { if (index >= k) { Node newNode = new Node(num); newNode.prev = node; node.next = newNode; } if (null == node.prev.prev) { //第一個節點就符合條件 node = node.nodeValue; continue; } node = node.prev.nodeValue; ++ index; continue; } node = node.next; } } private int generatorLevelK() { Random random = new Random(); return random.nextInt(level); }
至此,我們實現了一個跳錶的定義,初始化,查找,節點新增與刪除。
公衆號鏈接:https://mp.weixin.qq.com/s/cRI1COJFOopXVmz8mJL0Iw