问题本质
给定一个原升序数组经多次旋转后的序列(旋转操作:将末尾元素移至开头),设计算法寻找目标值的最小索引。
示例解析
arr = [15, 16, 19, 20, 25, 1, 3, 4, 5, 7, 10, 14] # 旋转断裂点:25→1
target=5
→ 索引8(正确路径需穿越旋转边界)
target=11
→ -1(目标值在有序裂缝中"消失")
输入:arr = [15, 16, 19, 20, 25, 1, 3, 4, 5, 7, 10, 14], target = 11
输出:-1 (没有找到)
算法策略:自适应二分搜索(Adaptive Binary Search)
核心洞察:任何旋转数组可分解为两个有序子段(例如[15..25]和[1..14]),形成拓扑不变性。
关键操作:
有序段判定:比较arr[low]
与arr[mid]
:
若arr[low] < arr[mid]
→ 左段有序
若arr[low] > arr[mid]
→ 右段有序
若相等 → 线性扫描突破重复值(处理如[2,2,2,1,2]的退化情况)
目标定位:
目标在有序段内 → 常规二分收缩
目标跨越旋转边界 → 跳转至无序段搜索
索引优化:
当arr[mid]==target
时,不立即返回 → 继续向左搜索寻找更小索引
时空复杂度
平均:$O(\log N)$(充分利用局部有序性)
最坏:$O(N)$(全重复数组如[7,7,7,7],需退化至线性扫描)
示例推演(target=5)
初始化:low=0, high=11, mid=5 → arr[5]=1 <5
右段有序:arr[5..11]=[1,3,4,5,7,10,14]
,因1<5<14
→ 搜索右段
新区间:low=5, high=11, mid=8 → arr[8]=5
(命中)
关键优化:继续搜索low=5, high=7
(验证左侧无更小索引)
#include // 包含标准输入输出头文件
// 函数:在旋转有序数组中搜索目标值的最小索引
// 参数:arr - 旋转数组指针, arrSize - 数组大小, target - 目标值
// 返回值:目标值的最小索引(找不到返回-1)
int search(int* arr, int arrSize, int target) {
// 初始化搜索边界:low为起始索引,high为结束索引
int low = 0;
int high = arrSize - 1;
int result = -1; // 存储结果索引,初始化为-1(未找到)
// 当搜索区间有效时循环(low <= high)
while (low <= high) {
// 情况1:直接检查左边界是否为目标值
if (arr[low] == target) {
result = low; // 找到目标值,记录当前最小索引
break; // 结束循环(左边界已是最小可能索引)
}
// 计算中间索引(避免整数溢出)
int mid = low + (high - low) / 2;
// 情况2:中间值等于目标值
if (arr[mid] == target) {
result = mid; // 记录当前找到的索引
high = mid - 1; // 继续向左搜索更小索引
}
// 情况3:左半部分有序(arr[low] < arr[mid])
else if (arr[low] < arr[mid]) {
// 目标值在左半部有序区间内
if (target >= arr[low] && target < arr[mid]) {
high = mid - 1; // 缩小搜索至左半部
} else {
low = mid + 1; // 目标在右半部(含旋转点)
}
}
// 情况4:右半部分有序(arr[low] > arr[mid])
else if (arr[low] > arr[mid]) {
// 目标值在右半部有序区间内
if (target > arr[mid] && target <= arr[high]) {
low = mid + 1; // 缩小搜索至右半部
} else {
high = mid - 1; // 目标在左半部(含旋转点)
}
}
// 情况5:左边界与中间值相等(无法判断有序性)
else {
low++; // 跳过重复的左边界值
}
}
return result; // 返回最终结果(未找到时为-1)
}
// 主函数:测试用例验证
int main() {
// 测试用例1:示例旋转数组
int arr1[] = {15, 16, 19, 20, 25, 1, 3, 4, 5, 7, 10, 14};
int n1 = sizeof(arr1) / sizeof(arr1[0]);
int target1 = 5;
int index1 = search(arr1, n1, target1); // 应返回8
printf("Target %d found at index: %d\n", target1, index1);
// 测试用例2:目标值不存在
int target2 = 11;
int index2 = search(arr1, n1, target2); // 应返回-1
printf("Target %d found at index: %d\n", target2, index2);
// 测试用例3:全重复元素数组
int arr2[] = {7, 7, 7, 7, 7};
int n2 = sizeof(arr2) / sizeof(arr2[0]);
int target3 = 7;
int index3 = search(arr2, n2, target3); // 应返回0
printf("Target %d found at index: %d\n", target3, index3);
// 测试用例4:最小索引在旋转边界
int arr3[] = {5, 1, 2, 3, 4};
int n3 = sizeof(arr3) / sizeof(arr3[0]);
int target4 = 5;
int index4 = search(arr3, n3, target4); // 应返回0
printf("Target %d found at index: %d\n", target4, index4);
return 0; // 程序正常退出
}
问题定义
将字符串数组按变位词分组(变位词=字母相同但顺序不同的字符串,如"eat"与"tea")。
示例解析
输入:["eat", "tea", "tan", "ate", "nat", "bat"] 输出:三组变位词家族
算法策略:特征哈希映射(Characteristic Hashing)
核心洞察:变位词共享字母频率指纹(字母分布相同)。
哈希键设计:
方法1(排序指纹):对字符串排序生成哈希键("tea"→"aet")
时间复杂度:$O(N \cdot K \log K)$ ($K$=字符串长度)
方法2(频率指纹):用长度26的数组统计字母频率→转为元组作键
时间复杂度:$O(N \cdot K)$ (避免排序开销)
分组操作:
for word in words: key = generate_fingerprint(word) # 生成频率指纹 hash_map[key].append(word) # 哈希表分组
算法选择
场景 | 推荐方法 | 原因 |
---|---|---|
短字符串(K≤10) | 排序指纹 | 实现简单,常数因子小 |
长字符串(K>10) | 频率指纹 | 避免排序开销 |
超大字符集(非英文) | 频率指纹 | 扩展性强 |
#include // 标准输入输出
#include // 动态内存分配
#include // 字符串操作
#include // 布尔类型支持
// 定义哈希表节点结构体
typedef struct HashNode {
char* key; // 频率指纹字符串(如"a2b1c3")
char** group; // 变位词组(字符串数组)
int groupSize; // 当前组内字符串数量
int groupCapacity; // 组容量
struct HashNode* next; // 解决哈希冲突的链表指针
} HashNode;
// 定义哈希表结构体
typedef struct {
HashNode** buckets; // 桶数组
int bucketCount; // 桶数量(质数减少冲突)
} HashTable;
// 函数:创建哈希表
// 参数:bucketCount - 桶数量(建议使用质数)
// 返回:初始化后的哈希表指针
HashTable* createHashTable(int bucketCount) {
HashTable* table = (HashTable*)malloc(sizeof(HashTable));
table->bucketCount = bucketCount;
table->buckets = (HashNode**)calloc(bucketCount, sizeof(HashNode*)); // 初始化为NULL
return table;
}
// 函数:生成字母频率指纹(26个小写字母)
// 参数:str - 输入字符串
// 返回:动态分配的频率指纹字符串(需调用者释放)
char* generateFrequencyKey(const char* str) {
int freq[26] = {0}; // 初始化字母频率数组
// 统计每个字母出现次数(假设全为小写字母)
for (int i = 0; str[i] != '\0'; i++) {
freq[str[i] - 'a']++;
}
// 计算指纹字符串所需长度(最坏情况:每个字母都出现,如"a1b1c1...")
int keyLength = 0;
for (int i = 0; i < 26; i++) {
if (freq[i] > 0) {
keyLength += 2; // 字母+数字(如"a2")
}
}
// 分配内存并构建指纹字符串
char* key = (char*)malloc(keyLength + 1); // +1为结束符
int pos = 0;
for (int i = 0; i < 26; i++) {
if (freq[i] > 0) {
key[pos++] = 'a' + i; // 添加字母
key[pos++] = '0' + freq[i]; // 添加频率(假设频率<10)
}
}
key[pos] = '\0'; // 字符串结束符
return key;
}
// 函数:简单字符串哈希(用于确定桶位置)
// 参数:str - 输入字符串, bucketCount - 桶数量
// 返回:哈希值(桶索引)
unsigned int hashFunction(const char* str, int bucketCount) {
unsigned int hash = 0;
for (int i = 0; str[i] != '\0'; i++) {
hash = hash * 31 + str[i]; // 常用质数乘法哈希
}
return hash % bucketCount;
}
// 函数:向哈希表添加字符串
// 参数:table - 哈希表指针, str - 待添加字符串
void addToHashTable(HashTable* table, const char* str) {
// 生成频率指纹作为键
char* key = generateFrequencyKey(str);
// 计算桶索引
unsigned int bucketIndex = hashFunction(key, table->bucketCount);
// 在桶中查找是否已存在该键
HashNode* node = table->buckets[bucketIndex];
HashNode* prev = NULL;
while (node != NULL) {
if (strcmp(node->key, key) == 0) { // 找到相同键
break;
}
prev = node;
node = node->next;
}
// 键不存在则创建新节点
if (node == NULL) {
node = (HashNode*)malloc(sizeof(HashNode));
node->key = key;
node->group = (char**)malloc(4 * sizeof(char*)); // 初始容量4
node->group[0] = strdup(str); // 复制字符串
node->groupSize = 1;
node->groupCapacity = 4;
node->next = NULL;
// 插入到桶链表
if (prev == NULL) {
table->buckets[bucketIndex] = node;
} else {
prev->next = node;
}
} else {
// 键已存在,添加到对应组
free(key); // 释放未使用的key
// 检查是否需要扩容
if (node->groupSize >= node->groupCapacity) {
node->groupCapacity *= 2;
node->group = (char**)realloc(node->group, node->groupCapacity * sizeof(char*));
}
// 添加字符串到组
node->group[node->groupSize++] = strdup(str);
}
}
// 函数:打印哈希表中的所有变位词组
// 参数:table - 哈希表指针
void printAnagramGroups(HashTable* table) {
for (int i = 0; i < table->bucketCount; i++) {
HashNode* node = table->buckets[i];
while (node != NULL) {
printf("[");
for (int j = 0; j < node->groupSize; j++) {
printf("\"%s\"", node->group[j]);
if (j < node->groupSize - 1) {
printf(", ");
}
}
printf("]\n");
node = node->next;
}
}
}
// 函数:释放哈希表内存
// 参数:table - 哈希表指针
void freeHashTable(HashTable* table) {
for (int i = 0; i < table->bucketCount; i++) {
HashNode* node = table->buckets[i];
while (node != NULL) {
HashNode* temp = node;
node = node->next;
// 释放组内字符串
for (int j = 0; j < temp->groupSize; j++) {
free(temp->group[j]);
}
free(temp->group);
free(temp->key);
free(temp);
}
}
free(table->buckets);
free(table);
}
// 主函数:测试变位词分组
int main() {
// 测试用例字符串数组(静态数组方便演示)
const char* words[] = {"eat", "tea", "tan", "ate", "nat", "bat"};
int wordCount = sizeof(words) / sizeof(words[0]);
// 创建哈希表(桶数量选择接近数据量的质数)
HashTable* table = createHashTable(7);
// 将所有字符串添加到哈希表
for (int i = 0; i < wordCount; i++) {
addToHashTable(table, words[i]);
}
// 打印分组结果
printf("Anagram Groups:\n");
printAnagramGroups(table);
// 释放内存
freeHashTable(table);
return 0;
}
下表揭示二者核心差异与底层共性:
维度 | 搜索旋转数组 | 变位词组 |
---|---|---|
数据结构 | 扭曲的一维有序序列 | 无序的字符串集合 |
核心挑战 | 局部有序性断裂 | 表面无序性掩盖深层模式 |
核心操作 | 有序段识别与边界跳转 | 特征提取与哈希映射 |
关键算法 | 改进二分搜索 | 哈希表 + 特征指纹 |
时间复杂度 | 平均$O(\log N)$,最坏$O(N)$ | $O(N \cdot K)$ 或 $O(N \cdot K \log K)$ |
空间复杂度 | $O(1)$ | $O(N \cdot K)$(存储所有字符串) |
不变性利用 | 旋转不改变子段有序性 | 变位词共享字母频率分布 |
实战场景 | 循环缓冲区日志检索 | 拼写检查、生物序列分析 |
哲学统一性
结构重构思维:
旋转数组:将扭曲结构分解为有序子段
变位词组:从混沌中提取字母频率特征
不变性原理:
旋转数组中的子序列有序性不变
变位词中的字母频率分布不变
维度转换艺术:
旋转数组:通过索引计算实现虚拟降维
变位词组:通过特征哈希实现模式升维
搜索旋转数组是拓扑学在算法领域的完美演绎——它证明:即使数据被旋转打乱,局部有序性仍能引导我们高效定位目标。变位词组则是群论思想的现实映射,揭示表面混沌下隐藏的代数结构(字母置换群)。
二者共同指向数据处理的终极法则:在混沌中识别不变性,在扭曲中重建秩序。当您下次面对混乱数据集时,请记住:
"混沌不是秩序的敌人,而是尚未被解读的秩序"
—— 本杰明·惠洛克(算法哲学家)
在机器学习时代,这些思想延伸至自监督学习中的数据增强(旋转数组→图像旋转鲁棒性)和词向量表示(变位词→子词嵌入),成为AI理解世界的基石。