來一起寫一個跳錶吧

Carol淋發表於2022-05-05

  跳錶全稱叫做跳躍表,簡稱跳錶,是一個隨機化的資料結構,實質就是一種可以進行二分查詢的有序連結串列。跳錶在原有的有序列表上面增加多級索引,通過索引來實現快速查詢。跳錶不僅能提高搜尋效能,同時也提高插入和刪除的效能,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;
    }
}
  初始化一個跳錶:跳錶的建立需要在資料有序的基礎上,然後從下往上在下一層的基礎上,間隔k生成當前層的節點,新生成的節點需要與當前層上一個節點連線起來,並且指向生成它的下一層節點。
/**
 * 原始資料連結串列
 */
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();
}
   從跳錶中查詢元素:從最頂層索引連結串列開始查詢,找到第一個大於當前節點的元素,則需要查詢的元素在當前節點與之前節點之間,則從當前節點的上一個節點prev往下nodevalue繼續進行查詢,直到當前節點值與查詢值相等,則直接返回當前節點,返回的節點可能是索引節點,也可能是原始資料節點,如果需要找到原始資料節點,則通過nodeValue繼續往下找。
/**
 * 是否存在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;
}
   新增節點:新增節點時候,如果不對索引進行調整,極端情況下,每次新增的節點都在之前第一層兩個節點之間,當這之間的連結串列越變越長,時間複雜度直接退化為O(n),所以需要同時新增索引,維持跳錶的高效性。但是我們如何新增,有一個方法就是,在新增節點時,隨機選擇k,即第k級索引,從1~k新增索引。
/**
 * 首先需要查詢插入位置,如果比最小的還小,直接在前面插入
 * 否則需要從最頂級一直查詢到資料連結串列,找到插入位置,插入,在查詢的過程中,就可以開始插入索引節點,
 * 從上往下進行插入
 * @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

相關文章