점프 테이블--C++ 구현

목차

작가가 할말이 있다

왜 점프워치를 배워야 할까요? 더 빨라지고, 더 빨라지고, 나 자신을 괴롭히기 위해...

점프 테이블 동작 장면 

1. 많은 회사들이 자체적으로 해시 테이블을 설계할 것이며 해시 충돌을 해결하는 것은 불가피합니다. 보통은 링크 주소를 사용하는데, 충돌이 있을 때 추가적인 연결 리스트에 1비트를 추가하는 것은 잘 알려져 있습니다. 단일 연결 목록인 경우 헤드 보간법을 사용하여 헤드에 직접 추가할 수도 있는데, 이는 매우 효율적입니다(O(1), 점프 테이블을 사용하는 경우 O(logN) 필요).

2. 이것을 보고 많은 친구들이 시계를 뛰어야 한다고 느끼고 속도가 느려질 것입니다. 걱정하지 마세요. 계속해서 살펴보겠습니다. 먼저 간단한 머리 차이 방법을 사용하고, 획득한 시퀀스는 순서가 잘못된 것으로 간주될 수 있습니다(순차 삽입 순서에 관계없이). 그런 다음 검색이 더 힘들어지므로 O(N)이 필요합니다. . 그러나 건너뛰기 테이블 조회를 사용하는 평균 효율은 O(logN), 삽입 O(logN), 삭제 O(logN)입니다.

[여기에 쓰여진 것은 평균적인 상황이라는 점에 유의하십시오. 점프 테이블이 잘 설계되지 않으면 점프 테이블이 연결 목록으로 변질되기 쉽습니다.]

3. 점프 테이블은 때때로 레드-블랙 트리와 AVL 트리를 대체할 수 있습니다 점프 테이블의 삽입 및 삭제 유지는 AVL 트리보다 낮고 레드-블랙 트리와 유사하다고 합니다. .

 스킵 테이블의 주요 아이디어

2점! 2점! 그래도 2점!

오랫동안 프로그래밍과 접하다 보면 이분법적 사고를 바탕으로 수많은 훌륭한 알고리즘이 진화하고 있음을 알게 될 것입니다.

알고리즘을 선택하는 방법은 특정 비즈니스 상황과 결합되어야 하며 알고리즘의 최고 시간 복잡도, 평균 시간 복잡도 및 최악의 시간 복잡도를 고려해야 합니다. 일반적으로 시간 복잡성이 O(logN)이라고 생각하더라도 서로 다른 애플리케이션 시나리오에서 동일한 기능을 가진 두 알고리즘 사이에는 큰 차이가 있습니다.

시간복잡도와 유사한 개념으로 공간복잡도가 있으며, 공간복잡도 역시 고려해야 할 문제이다. 하나는 CPU 점유, 하나는 메모리 점유…

공간 복잡성은 항상 시간 복잡성과 절충되며, 그들은 한 쌍의 적입니다. 그러나 일반적으로 허용 가능한 메모리 소비 내에서 가장 빠른 알고리즘을 선택하는 것이 일반적입니다. 세계 최고의 무술은 빠르다...

스킵 테이블의 특징

1. 단일 연결 리스트

2. 질서정연한 보존(이분법의 전제조건)

3. 추가, 삭제 및 검색 지원

4. 검색 효율 O(logN)

 점프 리스트와 일반 연결 리스트의 차이점

일반적인 단일 연결 리스트

 

         일반 단일 연결 목록의 경우 노드가 빠를수록 검색 속도가 빨라집니다. 나중 노드의 경우 검색 효율성이 낮습니다. 평균 효율 = (1+ 2 + 3 + ... + n) / n = (1/2 * n(a1+an))/n = (1 + n) / 2 ==> O(N)

 

간단한 스킵 테이블

   

         점프 테이블의 구조는 어떤 요소의 검색이 선형 시간 O(logN)에 근접하도록 고위도 인덱스를 설정하여 저위도를 줄이는 것입니다. 사실 스킵 테이블의 개념은 복잡하지 않은데 검색 효율을 높이기 위해 중간 노드의 차원을 늘리고 검색 과정에서 검색 과정을 점차 반으로 줄인다.

스킵 테이블 생성 

        점프 테이블의 기본 아이디어를 알고 나면 수동으로 점프 테이블을 시뮬레이션하고 빌드해 봅시다. 이제 7개의 요소 1 5 8 3 2 7 9를 순서대로 삽입합니다. ===> [고위도 노드를 1 간격으로 추출]

        1. 초기화 ==> 헤드 노드 준비 연결 리스트에는 헤드 노드가 있는 것과 헤드 노드가 없는 두 가지 방법이 있음을 알고 있습니다. 첫 번째 노드의 작업을 용이하게 하기 위해 헤드를 균일하게 사용합니다 마디).

          참고: 포인터 필드는 여기에 그려지지 않습니다.

        2. Insert 1 ==> 단일 연결 리스트처럼 바로 오른쪽에 노드를 삽입하면 됩니다.

         참고: 빈 부분은 와일드 포인터를 방지하기 위해 포인터가 NULL을 가리킨다는 것을 의미합니다.

        3. Insert 5 ==> 여기서는 요소를 위쪽으로 추출할 것인지(추출하지 않을 것인지) 고려하여 먼저 검색할 위치를 찾아 5 > 1이므로 1 뒤에 삽입하면 됩니다.

           ==> 다음 코드의 일관성을 보장하고 코드를 작성할 때 상황을 하나 덜 고려하기 위해 여전히 5개를 추출합니다.

          참고: 빈 부분은 와일드 포인터를 방지하기 위해 포인터가 NULL을 가리킨다는 것을 의미합니다.

        4. Insert 8 ==> 여전히 같은 이유로, 8을 찾으려면 검색해야 할 위치를 추출해야 합니다. 8 > 5 5는 미리 개선되어 있기 때문에 1을 비교한 후 5를 비교할 필요가 없습니다. 5를 직접 구하면 5 뒤에 8을 삽입해야 함을 알 수 있으며 점핑 테이블의 장점이 나타납니다.

        [이 과정의 삽입 과정은 2단계로 나뉘는데, 먼저 1차원 연결 리스트에 8을 삽입]

         참고: 빈 부분은 와일드 포인터를 방지하기 위해 포인터가 NULL을 가리킨다는 것을 의미합니다.

      참고 : 공백 부분은 와일드 포인터를 방지하기 위해 포인터가 NULL을 가리키는 것을 의미합니다.

        5. Insert 3 ==> 먼저 5 > 3을 비교하면 전체 범위가 [1-5]가 되고 1 < 3 < 5가 됩니다. 그런 다음 1과 5 사이에 3을 삽입해야 합니다.

        [삽입 3, 두 개의 추출 원칙에 따라 3을 업그레이드해야 하고 3과 5가 같은 레이어에 있으면 5를 업그레이드해야 합니다.]

        참고 : 공백 부분은 와일드 포인터를 방지하기 위해 포인터가 NULL을 가리키는 것을 의미합니다.

        6. Insert 2 ==> 과정은 위와 동일합니다. 너무 많은 설명이 필요없다...

        [ 그림 그리는게 쉬운게 아니니 참고만 하세요....고퀄의 글만 출력합니다 : 알고리즘에 대한 생각이 있으신 분은 아래에 글을 남겨주세요 ]

        참고: 빈 부분은 와일드 포인터를 방지하기 위해 포인터가 NULL을 가리킨다는 것을 의미합니다.

        7. 7 삽입 ==> 과정은 위와 동일합니다. 너무 많은 설명이 필요없다...

        참고: 빈 부분은 와일드 포인터를 방지하기 위해 포인터가 NULL을 가리킨다는 것을 의미합니다.

       8. 9 삽입 ==> 과정은 위와 동일합니다. 너무 많은 설명이 필요없다... 

        참고: 빈 부분은 와일드 포인터를 방지하기 위해 포인터가 NULL을 가리킨다는 것을 의미합니다.

      [위는 점프 테이블을 설정하는 전체 과정입니다. 이해가 되지 않으면 WeChat C++ 기술 교류 그룹에 가입할 수 있습니다: C++ 기술 교류 그룹-첸 아저씨 ]

       [ Chen Dashu 삼촌을 찾는 bilibili의 비디오는 나중에 업데이트 될 예정입니다. 관심을 가져 주셔서 감사합니다 ... 만들기가 쉽지 않습니다. 그냥 따라 가세요 ... ]

테이블 조회 건너뛰기

  점프 테이블의 조회는 이진 검색의 논리인 매우 간단합니다. 검색 프로세스를 더 잘 이해하기 위해 예를 살펴보겠습니다...

 예 1: 3 찾기

  첫 번째 비교 5 ==> 3 < 5 ==> 검색 범위는 1-5 사이가 됨

  그런 다음 헤더를 한 비트 아래로 이동하고 비교 3 == 3 ==> 3을 찾아 true를 반환합니다.

  

  예 2: 9 찾기 

  

건너뛰기 목록 삭제

  점프 테이블의 조회는 이진 검색의 논리인 매우 간단합니다. 검색 프로세스를 더 잘 이해하기 위해 예를 살펴보겠습니다...

  예 1: 삭제 2

  

 지속적인 증가와 레이어 수가 너무 많은 것을 방지하기 위해 조정할 수 있습니다.

스킵 테이블의 추출된 차원에 대한 설명

        점프 테이블의 구현이 시간과 공간을 교환하는 데이터에 대한 상향 중복 작업의 전형적인 예임을 찾는 것은 어렵지 않습니다.

우리가 설정한 중복 입자가 작을수록 필요한 공간이 더 커집니다. 따라서 특정 상황에 따라 결정해야 하는 좋은 중복 입도를 선택하는 것이 매우 중요합니다.

        재점프 테이블에는 임의의 기능이 있는데, 그 의미는 상향 중복성을 만드는 것이 필요한 시점을 결정하는 것입니다. 실제 건설 과정은 우리의 예와 같지 않을 것이므로 비용이 상대적으로 높을 것입니다. 삽입된 노드의 증가로 인해 점프 테이블이 연결 리스트로 퇴보하는 것을 방지하기 위해 보통 랜덤 함수를 사용하여 업그레이드 시점을 결정합니다.

점프 테이블 코드 

#pragma once
#ifndef SKIPLIST_ENTRY_H_
#define SKIPLIST_ENTRY_H_
/* 一个更具备代表性的泛型版本 */
#include <ctime>
#include <cstdlib>

template<typename T>
class Entry {
private:
    int key; // 排序值
    T value; // 保存对象
    Entry* pNext;
    Entry* pDown;
public:
    
    Entry(int k, T v) :value(v), key(k), pNext(nullptr), pDown(nullptr) {}
    
    Entry(const Entry& e) :value(e.value), key(e.key), pNext(nullptr), pDown(nullptr) {}

public:
    /* 重载运算符 */
    bool operator<(const Entry& right) {
        return key < right.key;
    }
    bool operator>(const Entry& right) {
        return key > right.key;
    }
    bool operator<=(const Entry& right) {
        return key <= right.key;
    }
    bool operator>=(const Entry& right) {
        return key >= right.key;
    }
    bool operator==(const Entry& right) {
        return key == right.key;
    }
    Entry*& next() {
        return pNext;
    }
    Entry*& down() {
        return pDown;
    }
};

template<typename T>
class SkipList_Entry {
private:
    struct Endpoint {
        Endpoint* up;
        Endpoint* down;
        Entry<T>* right;
    };
    struct Endpoint* header;
    int lvl_num; // level_number 已存在的层数
    unsigned int seed;
    bool random() {
        srand(seed);
        int ret = rand() % 2;
        seed = rand();
        return ret == 0;
    }
public:
    SkipList_Entry() :lvl_num(1), seed(time(0)) {
        header = new Endpoint();
    }
    /* 插入新元素 */
    void insert(Entry<T>* entry) { // 插入是一系列自底向上的操作
        struct Endpoint* cur_header = header;
        // 首先使用链表header到达L1
        while (cur_header->down != nullptr) {
            cur_header = cur_header->down;
        }
        /* 这里的一个简单想法是L1必定需要插入元素,而在上面的各跳跃层是否插入则根据random确定
           因此这是一个典型的do-while循环模式 */
        int cur_lvl = 0; // current_level 当前层数
        Entry<T>* temp_entry = nullptr; // 用来临时保存一个已经完成插入的节点指针
        do {
            Entry<T>* cur_cp_entry = new Entry<T>(*entry); // 拷贝新对象
            // 首先需要判断当前层是否已经存在,如果不存在增新增
            cur_lvl++;
            if (lvl_num < cur_lvl) {
                lvl_num++;
                Endpoint *new_header = new Endpoint();
                new_header->down = header;
                header->up = new_header;
                header = new_header;
            }
            // 使用cur_lvl作为判断标准,!=1表示cur_header需要上移并连接“同位素”指针
            if (cur_lvl != 1) {
                cur_header = cur_header->up;
                cur_cp_entry->down() = temp_entry;
            }
            temp_entry = cur_cp_entry;
            // 再需要判断的情况是当前所在链表是否已经有元素节点存在,如果是空链表则直接对右侧指针赋值并跳出循环
            if (cur_header->right == nullptr) {
                cur_header->right = cur_cp_entry;
                break;
            }
            else {
                Entry<T>* cursor = cur_header->right; // 创建一个游标指针
                while (true) { // 于当前链表循环向右寻找可插入点,并在找到后跳出当前循环
                    if (*cur_cp_entry < *cursor) { // 元素小于当前链表所有元素,插入链表头
                        cur_header->right = cur_cp_entry;
                        cur_cp_entry->next() = cursor;
                        break;
                    }
                    else if (cursor->next() == nullptr) { // 元素大于当前链表所有元素,插入链表尾
                        cursor->next() = cur_cp_entry;
                        break;
                    }
                    else if (*cur_cp_entry < *cursor->next()) { // 插入链表中间
                        cur_cp_entry->next() = cursor->next();
                        cursor->next() = cur_cp_entry;
                        break;
                    }
                    cursor = cursor->next(); // 右移动游标
                }
            }
        } while(random());
    }

    /* 查询元素 */
    bool search(Entry<T>* entry) const {
        if (header->right == nullptr) { // 判断链表头右侧空指针
            return false;
        }
        Endpoint* cur_header = header;
        // 在lvl_num层中首先找到可以接入的点
        for (int i = 0; i < lvl_num; i++) {
            if (*entry < *cur_header->right) {
                cur_header = cur_header->down;
            }
            else {
                Entry<T>* cursor = cur_header->right;
                while (cursor->down() != nullptr) {
                    while (cursor->next() != nullptr) {
                        if (*entry <= *cursor->next()) {
                            break;
                        }
                        cursor = cursor->next();
                    }
                    cursor = cursor->down();
                }
                while (cursor->next() != nullptr) {
                    if (*entry > *cursor->next()) {
                        cursor = cursor->next();
                    }
                    else if (*entry == *cursor->next()) {
                        return true;
                    }
                    else {
                        return false;
                    }
                }
                return false; // 节点大于L1最后一个元素节点,返回false
            }
        }
        return false; // 找不到接入点,则直接返回false;
    }
    /* 删除元素 */
    void remove(Entry<T>* entry) {
        if (header->right == nullptr) {
            return;
        }
        Endpoint* cur_header = header;
        Entry<T>* cursor = cur_header->right;
        int lvl_counter = lvl_num; // 因为在删除的过程中,跳跃表的层数会中途发生变化,因此应该在进入循环之前要获取它的值。
        for (int i = 0; i < lvl_num; i++) {
            if (*entry == *cur_header->right) {
                Entry<T>* delptr = cur_header->right;
                cur_header->right = cur_header->right->next();
                delete delptr;
            }
            else {
                Entry<T> *cursor = cur_header->right;
                while (cursor->next() != nullptr) {
                    if (*entry == *cursor->next()) { // 找到节点->删除->跳出循环
                        Entry<T>* delptr = cursor->next();
                        cursor->next() = cursor->next()->next();
                        delete delptr;
                        break;
                    }
                    cursor = cursor->next();
                }
            }
            // 向下移动链表头指针的时候需要先判断当前链表中是否还存在Entry节点
            if (cur_header->right == nullptr) {
                Endpoint* delheader = cur_header;
                cur_header = cur_header->down;
                header = cur_header;
                delete delheader;
                lvl_num--;
            }
            else {
                cur_header = cur_header->down;
            }
        }
    }
};
#endif // !SKIPLIST_ENTRY_H_

    C++ 학습에 있어 가장 큰 문제 중 하나는 소통할 수 있는 사람이 적고 온라인 자료도 비교적 적다는 것입니다.

   C++에 대해 의구심이 있거나 친구와 소통하고 싶다면 V: Errrr113에 가입하세요. 

    원래 의도를 고수하고 용감하고 단호하게...기술을 사랑하는 모든 친구에게! ! !

추천

출처blog.csdn.net/weixin_46120107/article/details/129311076