1 算法题 :使用跳表查找算法在有序数组中查找指定元素
1.1 题目含义
本题要求实现一个跳表(Skip List)数据结构,并使用该跳表在有序数组中查找指定的元素,返回该元素在数组中的位置(索引)。跳表是一种随机化的数据结构,它允许快速查询、插入、删除和更新一个有序列表的元素。
跳表的基本结构和特性
跳表通过维护多个有序的链表来加速查找过程。每个链表(或称为层级)包含原始数据集的一个子集,并且这些链表通过指针相互连接。最底层的链表包含所有的元素,而每一层更上层的链表则是下一层链表的子集,且节点数量大约只有下一层的一半。这种结构使得在跳表中查找元素的时间复杂度接近 O(log n)。
1.2 示例
示例 1:
输入:
- nums = [-1, 0, 3, 5, 9, 12]
- target = 9
输出:
- 4
说明:
- 9 存在于 nums 中,且其索引为 4。
示例 2:
输入:
- nums = [-1, 0, 3, 5, 9, 12]
- target = 2
输出:
- -1
说明:
- 2 不存在于 nums 中,返回 -1。
示例 3:
输入:
- nums = []
- target = 0
输出:
- -1
说明:
- nums 为空,0 不存在于 nums 中,返回 -1。
2 解题思路
一、基本概念
跳表(Skip List)是一种可以随机化构建的数据结构,它允许快速查询、插入、删除和更新一个有序列表的元素。跳表通过维护多个有序的链表来加速查找过程,使得在平均和最坏情况下的时间复杂度都接近O(log n)。本题要求我们在C++中实现跳表,并使用它在有序数组中查找指定元素,返回元素的位置。
二、跳表的基本结构与特性
跳表由多个有序链表组成,每个链表称为一个层级。最底层的链表包含所有的元素,而每一层更上层的链表则是下一层链表的子集,且节点数量大约只有下一层的一半。这种结构使得在跳表中查找元素时,可以逐层跳跃,从而快速定位到目标元素。
跳表的关键特性包括:
随机化:跳表的构建过程中使用了随机化技术,使得链表的层级和节点分布具有一定的随机性。这种随机性有助于平衡跳表的性能,使得在大多数情况下都能达到较高的查找效率。
有序性:跳表中的每个链表都是有序的,这保证了在查找过程中可以逐层缩小搜索范围,直至找到目标元素或确定元素不存在。
空间效率:跳表通过维护多个链表来加速查找,但并不会消耗过多的额外空间。因为每个节点的层级是随机确定的,所以整体空间复杂度与原始数据集的大小呈线性关系。
三、解题思路概述
本题的解题思路可以分为以下几个步骤:
定义跳表节点结构:首先,我们需要定义一个跳表节点的结构体,该结构体应包含数据域(存储元素的值)、指针域(指向同一层级和下一层级节点的指针)以及层级信息。
实现跳表的基本操作:接下来,实现跳表的基本操作,包括创建跳表、插入元素、删除元素和查找元素等。这些操作需要根据跳表的结构特性进行设计,确保能够在保持跳表有序性的同时,实现高效的查找和更新操作。
构建跳表与数组元素的映射关系:由于题目要求使用跳表在有序数组中查找元素,我们需要建立跳表节点与数组元素的映射关系。这可以通过在跳表节点中存储数组元素的索引或指针来实现。
实现查找指定元素位置的函数:最后,实现一个函数来查找指定元素在有序数组中的位置。该函数应遍历跳表的底层链表,与数组元素进行比较,直到找到目标元素或确定元素不存在。在查找过程中,可以利用跳表的层级结构来加速查找过程。
四、详细解题步骤
(1)定义跳表节点结构
定义一个结构体来表示跳表的节点,该结构体应包含以下成员:
- 数据域:存储元素的值。
- 指针域:包括指向同一层级下一个节点的指针和指向下一层级对应节点的指针。
- 层级信息:记录当前节点在跳表中的层级。
示例代码片段(伪代码):
struct SkipListNode {
int value; // 数据域
SkipListNode* next; // 指向同一层级下一个节点的指针
int level; // 层级信息
};
(2)实现跳表的基本操作
创建跳表:初始化一个空的跳表,通常只包含一个头节点。
插入元素:根据一定的随机化策略确定新节点的层级,并在对应层级中插入新节点,同时保持跳表的有序性。
删除元素:在跳表中查找要删除的元素,并逐层删除对应节点。
查找元素:从跳表的最高层开始逐层查找目标元素,直到找到元素或确定元素不存在。
这些操作需要仔细处理节点的插入和删除逻辑,以确保跳表的正确性和性能。
(3)构建跳表与数组元素的映射关系
由于题目要求使用跳表在有序数组中查找元素,我们需要建立跳表节点与数组元素的映射关系。这可以通过在跳表节点中存储数组元素的索引或指针来实现。在插入元素时,将数组元素的索引或指针与跳表节点关联起来;在查找元素时,通过跳表节点找到对应的数组元素索引或指针。
(4)实现查找指定元素位置的函数
实现一个函数来查找指定元素在有序数组中的位置。该函数应遍历跳表的底层链表,与数组元素进行比较,直到找到目标元素或确定元素不存在。在查找过程中,可以利用跳表的层级结构来加速查找过程。
具体实现时,可以从跳表的最高层开始逐层向下查找,逐层缩小搜索范围,直至到达底层链表。在底层链表中,逐个节点与数组元素进行比较,直到找到目标元素或遍历完整个链表。如果找到目标元素,则返回该元素在数组中的位置(索引);如果未找到目标元素,则返回-1表示未找到。
五、优化与注意事项
随机化策略:在插入元素时,为了平衡跳表的性能,需要使用随机化策略来确定新节点的层级。通常可以通过抛掷硬币或计算随机数的方式来决定节点是否继续向下层延伸。
空间复杂度:跳表的空间复杂度与原始数据集的大小呈线性关系。在实际应用中,需要根据内存限制和性能需求来选择合适的层级策略,避免过多的空间浪费。
时间复杂度:跳表的查找、插入和删除操作的时间复杂度在平均和最坏情况下都接近O(log n)。然而,由于随机化策略的存在,实际性能可能会有所波动。因此,在实现时需要注意保持跳表的平衡性,以获得稳定的性能表现。
错误处理与边界情况:在实现过程中,需要考虑各种错误处理和边界情况,如空跳表、空数组、重复元素等。这些情况需要在代码中进行特殊处理,以确保程序的正确性和健壮性。
性能优化:针对特定的应用场景和数据集特性,可以对跳表的实现进行性能优化。例如,可以通过调整随机化策略、优化节点插入和删除逻辑、使用更高效的数据结构来存储节点信息等方式来提升跳表的性能。
3 算法实现代码
如下为算法实现代码:
#include <iostream>
#include <vector>
#include <cmath>
#include <ctime>
#include <cstdlib>
class SkipList {
public:
SkipList(int maxLevel) : maxLevel(maxLevel), head(new Node(-1, maxLevel)) {}
~SkipList() {
Node* node = head;
while (node) {
Node* temp = node->next[0];
delete node;
node = temp;
}
}
void insert(int value) {
std::vector<Node*> update(maxLevel);
Node* node = head;
for (int i = maxLevel - 1; i >= 0; --i) {
while (node->next[i] && node->next[i]->value < value) {
node = node->next[i];
}
update[i] = node;
}
int newLevel = randomLevel();
if (newLevel > maxLevel) {
for (int i = maxLevel; i < newLevel; ++i) {
update.push_back(head);
}
maxLevel = newLevel;
}
node = new Node(value, newLevel);
for (int i = 0; i < newLevel; ++i) {
node->next[i] = update[i]->next[i];
update[i]->next[i] = node;
}
}
int search(int value) {
Node* node = head;
for (int i = maxLevel - 1; i >= 0; --i) {
while (node->next[i] && node->next[i]->value < value) {
node = node->next[i];
}
}
node = node->next[0];
return node && node->value == value ? node->value : -1;
}
private:
struct Node {
int value;
std::vector<Node*> next;
Node(int value, int level) : value(value), next(level, nullptr) {}
};
int randomLevel() {
int level = 1;
while (rand() % 2 && level < maxLevel) {
++level;
}
return level;
}
int maxLevel;
Node* head;
};
class Solution
{
public:
int skipSearch(std::vector<int>& nums, int target) {
SkipList skipList(4);
for (int value : nums) {
skipList.insert(value);
}
int position = skipList.search(target);
return position;
}
};
这段代码首先定义了一个SkipList类,它包含了以下成员变量和成员函数:
- maxLevel:跳表的最大层数。
- head:跳表的头节点,它是一个特殊的节点,它的值是-1,并且它的下一层指向第一个实际存储数据的节点。
- insert(int value):向跳表中插入一个数值。
- search(int value):在跳表中查找一个数值,如果找到了就返回该数值,否则返回-1。
在 SkipList 类中还定义了一个内部结构体 Node,表示跳表中的一个节点。每个节点包含一个整数值和一个指向下一个节点的指针数组。
randomLevel() 函数用于生成随机的层数,它通过循环生成一个随机数,如果随机数为奇数且当前层数小于最大层数,则层数加1,直到生成的层数达到最大层数或者随机数为偶数为止。
调用上面的算法,并得到输出:
int main() {
// 使用当前时间进行随机数发生器的初始化。
srand(time(nullptr));
Solution s;
std::vector<int> nums = { 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 };
int target = 3;
int position = s.skipSearch(nums,target);
if (position != -1) {
std::cout << "Target found at index: " << std::distance(nums.begin(), std::find(nums.begin(), nums.end(), target)) << std::endl;
}
else {
std::cout << "Target not found in the array." << std::endl;
}
return 0;
}
上面代码的输出为:
Target found at index: 1
4 测试用例
以下是针对上面算法的测试用例,基本覆盖了各种情况:
(1)基础测试用例
输入:数组 [-1, 0, 3, 5, 9, 12],目标值 9
输出:4
说明:目标值 9 存在于数组中,位于索引 4 的位置。
(2)目标值不存在于数组中
输入:数组 [-1, 0, 3, 5, 9, 12],目标值 2
输出:-1
说明:目标值 2 不存在于数组中。
(3)目标值位于数组开头
输入:数组 [-1, 0, 3, 5, 9, 12],目标值 -1
输出:0
说明:目标值 -1 位于数组的开头,即索引 0 的位置。
(4)目标值位于数组末尾
输入:数组 [-1, 0, 3, 5, 9, 12],目标值 12
输出:5
说明:目标值 12 位于数组的末尾,即索引 5 的位置。
(5)目标值位于数组中间
输入:数组 [-1, 0, 3, 5, 9, 12],目标值 3
输出:2
说明:目标值 3 位于数组的中间位置,即索引 2 的位置。
(6)空数组
输入:数组 [],目标值 9
输出:-1
说明:空数组不包含任何元素,因此无法找到目标值。
(7)数组只有一个元素
输入:数组 [9],目标值 9
输出:0
说明:数组只有一个元素,且该元素就是目标值,位于索引 0 的位置。
(8)数组中存在多个相同的目标值
输入:数组 [1, 2, 3, 3, 4, 5],目标值 3
输出:2 或 3
说明:数组中存在多个目标值 3,返回任意一个目标值的索引都是正确的。这里可以返回 2 或 3。