以下是关于LeetCode 49题(Group Anagrams,字母异位词分组)的详细介绍:
题目名称
Group Anagrams(字母异位词分组)
题目叙述
给定一个字符串数组 strs
,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。
例如,输入: ["eat", "tea", "tan", "ate", "nat", "bat"]
,输出: [["eat","tea","ate"],["tan","nat"],["bat"]]
。
模式识别(考点)
- 哈希表的运用:本题主要考查如何利用哈希表来对具有相同特征(这里是字母异位词)的元素进行分组存储,通过合适的哈希函数将不同的字符串映射到对应的分组中。
- 字符串处理与排序:需要对字符串进行操作,判断哪些字符串是字母异位词,常见的做法是对字符串中的字符进行排序,排序后相同的字符串即为字母异位词,以此作为哈希表中分组的依据。
解题方法过程
方法一:排序后作为哈希表的键值(常用方法)
- 思路:
- 遍历输入的字符串数组
strs
。 - 对于每个字符串,先将其字符进行排序(例如使用
qsort
函数对字符数组进行排序),排序后的字符串作为哈希表的键(可以使用char *
类型作为键,这里假设使用C
语言中自定义的哈希表结构来实现)。 - 如果哈希表中该键不存在,则创建一个新的列表(例如使用链表结构来存储属于同一组的字符串),并将当前字符串加入该列表;如果键已存在,则直接将字符串添加到对应的列表中。
- 最后遍历哈希表,将每个键对应的字符串列表取出,组成最终的结果数组返回。
- 遍历输入的字符串数组
- 代码实现如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 自定义链表节点结构体,用于存储同一组的字符串
typedef struct ListNode {
char *str;
struct ListNode *next;
} ListNode;
// 自定义哈希表结构体,简单起见,这里用数组模拟哈希表,实际应用中可按需优化
#define HASH_SIZE 10000 // 哈希表大小,可根据数据规模调整
typedef struct {
ListNode *buckets[HASH_SIZE];
} HashTable;
// 初始化哈希表,将每个桶的头节点设为 NULL
void hashTableInit(HashTable *ht) {
for (int i = 0; i < HASH_SIZE; i++) {
ht->buckets[i] = NULL;
}
}
// 哈希函数,简单地将字符串的哈希值取余得到桶的索引,这里只是示例,可优化
int hashFunction(char *s) {
unsigned int hash = 0;
for (int i = 0; s[i]!= '\0'; i++) {
hash = hash * 31 + s[i]; // 简单的哈希计算方式
}
return hash % HASH_SIZE;
}
// 在哈希表中插入字符串到对应的桶(链表)中
void hashTableInsert(HashTable *ht, char *s) {
int index = hashFunction(s);
ListNode *node = ht->buckets[index];
if (node == NULL) {
// 如果桶为空,创建新节点并插入
ListNode *newNode = (ListNode *)malloc(sizeof(ListNode));
newNode->str = strdup(s); // 复制字符串,避免原字符串被修改影响
newNode->next = NULL;
ht->buckets[index] = newNode;
} else {
// 如果桶不为空,遍历链表找到合适位置插入(简单的尾插法)
while (node!= NULL) {
if (strcmp(node->str, s) == 0) {
// 如果字符串已存在,不重复插入(假设不重复插入情况)
return;
}
if (node->next == NULL) {
break;
}
node = node->next;
}
ListNode *newNode = (ListNode *)malloc(sizeof(ListNode));
newNode->str = strdup(s);
newNode->next = node->next;
node->next = newNode;
}
}
// 释放哈希表中每个桶对应的链表内存,避免内存泄漏
void hashTableFree(HashTable *ht) {
for (int i = 0; i < HASH_SIZE; i++) {
ListNode *node = ht->buckets[i];
while (node!= NULL) {
ListNode *next = node->next;
free(node->str);
free(node);
node = next;
}
}
}
// 比较函数,用于qsort对字符串内字符排序
int compare(const void *a, const void *b) {
return (*(char *)a - *(char *)b);
}
// 题目中的函数,用于对字符串数组进行字母异位词分组
// 参数strs:输入的字符串数组,包含多个待分组的字符串
// 参数strsSize:输入字符串数组的元素个数,用于确定循环边界等操作
char *** groupAnagrams(char **strs, int strsSize) {
// 初始化哈希表
HashTable ht;
hashTableInit(ht);
// 遍历输入的字符串数组
for (int i = 0; i < strsSize; i++) {
char *s = strs[i];
int len = strlen(s);
// 复制一份原字符串,用于排序操作,避免修改原字符串
char *sorted = (char *)malloc((len + 1) * sizeof(char));
strcpy(sorted, s);
// 对复制的字符串进行字符排序,使其成为哈希表的键依据
qsort(sorted, len, sizeof(char), compare);
// 将字符串插入到哈希表对应的桶(链表)中
hashTableInsert(ht, sorted);
free(sorted); // 释放临时排序用的字符串内存
}
// 统计结果中分组的数量,用于分配结果数组的内存
int groupCount = 0;
for (int i = 0; i < HASH_SIZE; i++) {
if (ht.buckets[i]!= NULL) {
groupCount++;
}
}
// 分配结果数组的内存,外层数组存储每组的字符串数组指针
char ***result = (char ***)malloc((groupCount + 1) * sizeof(char **));
int index = 0;
// 遍历哈希表,将每个桶(链表)中的字符串提取出来组成结果数组
for (int i = 0; i < HASH_SIZE; i++) {
ListNode *node = ht.buckets[i];
if (node!= NULL) {
int listSize = 0;
ListNode *cur = node;
// 统计当前桶(链表)中字符串的数量,用于分配内层数组内存
while (cur!= NULL) {
listSize++;
cur = cur->next;
}
// 分配内层数组内存,用于存储当前组的字符串
char **group = (char **)malloc((listSize + 1) * sizeof(char *));
cur = node;
int j = 0;
// 将当前桶(链表)中的字符串复制到内层数组中
while (cur!= NULL) {
group[j++] = cur->str;
cur = cur->next;
}
group[j] = NULL; // 内层数组末尾设为 NULL,符合约定
result[index++] = group; // 将当前组的字符串数组指针存入结果数组
}
}
result[index] = NULL; // 结果数组末尾设为 NULL,符合约定
// 释放哈希表内存
hashTableFree(ht);
return result;
}
方法二:计数法(利用字符出现次数作为哈希表的键)
- 思路:
- 同样遍历字符串数组
strs
。 - 对于每个字符串,创建一个长度为 26 的整数数组(假设只处理小写英文字母,可根据实际情况扩展)来记录每个字母出现的次数,以此数组作为哈希表的键。
- 将该整数数组的每个元素转换为一个唯一的表示形式(例如可以拼接成一个字符串或者按照某种规则计算出一个唯一整数等),然后根据这个表示形式判断哈希表中对应的分组是否存在,不存在则创建新分组并加入当前字符串,存在则直接添加到对应分组。
- 最后按照和方法一类似的方式构建并返回结果数组。
- 同样遍历字符串数组
- 代码实现如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 自定义链表节点结构体,用于存储同一组的字符串
typedef struct ListNode {
char *str;
struct ListNode *next;
} ListNode;
// 自定义哈希表结构体,简单起见,这里用数组模拟哈希表,实际应用中可按需优化
#define HASH_SIZE 10000 // 哈希表大小,可根据数据规模调整
typedef struct {
ListNode *buckets[HASH_SIZE];
} HashTable;
// 初始化哈希表,将每个桶的头节点设为 NULL
void hashTableInit(HashTable *ht) {
for (int i = 0; i < HASH_SIZE; i++) {
ht->buckets[i] = NULL;
}
}
// 简单的哈希函数示例,将计数数组转换为整数来作为哈希表的索引,可优化
int hashFunction(int *counts) {
int hash = 0;
for (int i = 0; i < 26; i++) { // 假设只处理26个小写英文字母
hash = hash * 31 + counts[i]; // 简单的哈希计算方式
}
return hash % HASH_SIZE;
}
// 在哈希表中插入字符串到对应的桶(链表)中,根据字符计数数组来判断分组
void hashTableInsert(HashTable *ht, char *s, int *counts) {
int index = hashFunction(counts);
ListNode *node = ht->buckets[index];
if (node == NULL) {
// 如果桶为空,创建新节点并插入
ListNode *newNode = (ListNode *)malloc(sizeof(ListNode));
newNode->str = strdup(s); // 复制字符串,避免原字符串被修改影响
newNode->next = NULL;
ht->buckets[index] = newNode;
} else {
// 如果桶不为空,遍历链表找到合适位置插入(简单的尾插法)
while (node!= NULL) {
if (strcmp(node->str, s) == 0) {
// 如果字符串已存在,不重复插入(假设不重复插入情况)
return;
}
if (node->next == NULL) {
break;
}
node = node->next;
}
ListNode *newNode = (ListNode *)malloc(sizeof(ListNode));
newNode->str = strdup(s);
newNode->next = node->next;
node->next = newNode;
}
}
// 释放哈希表中每个桶对应的链表内存,避免内存泄漏
void hashTableFree(HashTable *ht) {
for (int i = 0; i < HASH_SIZE; i++) {
ListNode *node = ht->buckets[i];
while (node!= NULL) {
ListNode *next = node->next;
free(node->str);
free(node);
node = next;
}
}
}
// 题目中的函数,利用字符计数作为哈希依据来对字符串数组进行字母异位词分组
// 参数strs:输入的字符串数组,包含多个待分组的字符串
// 参数strsSize:输入字符串数组的元素个数,用于确定循环边界等操作
char *** groupAnagrams(char **strs, int strsSize) {
// 初始化哈希表
HashTable ht;
hashTableInit(ht);
// 遍历输入的字符串数组
for (int i = 0; i < strsSize; i++) {
char *s = strs[i];
int len = strlen(s);
// 创建长度为26的数组用于记录每个小写字母出现的次数
int counts[26] = {0};
// 统计当前字符串中每个字母出现的次数
for (int j = 0; j < len; j++) {
counts[s[j] - 'a']++;
}
// 将字符串插入到哈希表对应的桶(链表)中,根据字符计数情况分组
hashTableInsert(ht, s, counts);
}
// 统计结果中分组的数量,用于分配结果数组的内存
int groupCount = 0;
for (int i = 0; i < HASH_SIZE; i++) {
if (ht.buckets[i]!= NULL) {
groupCount++;
}
}
// 分配结果数组的内存,外层数组存储每组的字符串数组指针
char ***result = (char ***)malloc((groupCount + 1) * sizeof(char **));
int index = 0;
// 遍历哈希表,将每个桶(链表)中的字符串提取出来组成结果数组
for (int i = 0; i < HASH_SIZE; i++) {
ListNode *node = ht.buckets[i];
if (node!= NULL) {
int listSize = 0;
ListNode *cur = node;
// 统计当前桶(链表)中字符串的数量,用于分配内层数组内存
while (cur!= NULL) {
listSize++;
cur = cur->next;
}
// 分配内层数组内存,用于存储当前组的字符串
char **group = (char **)malloc((listSize + 1) * sizeof(char *));
cur = node;
int j = 0;
// 将当前桶(链表)中的字符串复制到内层数组中
while (cur!= NULL) {
group[j++] = cur->str;
cur = cur->next;
}
group[j] = NULL; // 内层数组末尾设为 NULL,符合约定
result[index++] = group; // 将当前组的字符串数组指针存入结果数组
}
}
result[index] = NULL; // 结果数组末尾设为 NULL,符合约定
// 释放哈希表内存
hashTableFree(ht);
return result;
}
以上两种方法都可以解决字母异位词分组的问题,方法一相对比较直观易懂,利用了排序后字符串相同则为字母异位词的特点;方法二则通过更细致地统计字符出现次数来判断,在某些场景下可能效率更高,具体可根据实际情况和输入数据的特点来选择使用。
请注意,上述代码中的哈希表实现等部分只是示例性质,在实际应用中可以使用更完善、高效的哈希表结构(比如 C++
中的 unordered_map
等),并且代码中的一些边界处理、内存管理等方面可以进一步优化,这里主要是为了展示解题思路和核心逻辑。