LeetCode第十七题:电话号码的字母组合

题目来源:LeetCode第十七题

1.题目描述

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

LeetCode第十七题:电话号码的字母组合插图

示例 1:

输入:digits = "23"

输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

提示:

  • 0 <= digits.length <= 4
  • digits[i] 是范围 ['2', '9'] 的一个数字。

解题思路

解法一:回溯法

  • 描述:准备一个映射表来将数字映射到对应的字母串,一个临时字符串用于存储当前的组合,和一个动态数组用于存储所有可能的组合。从输入的第一个数字开始,对于每个数字,根据映射表找到对应的所有可能的字母。遍历这些字母,将每个字母依次加入到当前的组合字符串中。对于加入了新字母的当前组合,递归地调用回溯函数,处理下一个数字。如果当前组合的长度等于输入数字串的长度,说明找到了一个完整的组合,将其加入到结果集中。在返回到上一层递归之前,撤销最后一步的选择(即从当前组合中移除最后一个字母),以便尝试其他可能的字母。
  • 时间复杂度:O(3^m * 4^n)mn分别代表输入数字中对应3个字母的数字个数和对应4个字母的数字个数。
  • 空间复杂度:O(m + n)。递归的最大深度等于输入数字字符串的长度,即m + n。因此,递归调用栈占用的空间是O(m + n)
  • 参考代码:

java版:

class Solution {
    // 数字到字母的映射
    private static final Map<Character, String> phoneMap = Map.of('2', "abc", '3', "def", '4', "ghi", '5', "jkl",
            '6', "mno", '7', "pqrs", '8', "tuv", '9', "wxyz");

    public List<String> letterCombinations(String digits) {
        // 存放所有字母组合
        List<String> combinations = new ArrayList<>();
        // 如果输入为空,则直接返回空的结果列表
        if (digits == null || digits.length() == 0) {
            return combinations;
        }
        // 开始回溯过程,从第0个索引和空的当前组合字符串开始
        backtrack(combinations, digits, 0, new StringBuilder());
        return combinations;
    }

    // 回溯方法
    private void backtrack(List<String> combinations, String digits, int index, StringBuilder combination) {
        // 如果当前索引等于数字字符串的长度,意味着已经处理完所有数字,当前组合完成
        // 例如23 处理2的a后索引为1,然后处理3的d,索引为2,继续回溯的时候index=digits.length()
        // 就把ad加入组合中
        if (index == digits.length()) {
            combinations.add(combination.toString());
        } else {
            // 获取当前数字
            char digit = digits.charAt(index);
            // 在Map中找这个数字对应的字符串
            String letters = phoneMap.get(digit);
            // 遍历当前数字对应的字符串的所有字母
            for (char letter : letters.toCharArray()) {
                // 将当前字母添加到组合中
                combination.append(letter);
                // 递归调用,处理下一个数字
                backtrack(combinations, digits, index + 1, combination);
                // 回溯:撤销上一步的选择(删除组合中的最后一个字母),尝试下一个可能的字母
                combination.deleteCharAt(index);
            }
        }
    }
}

C++版:

class Solution {
public:
	vector<string> letterCombinations(string digits) {
		vector<string> combinations;
		// 如果输入为空,则直接返回空的结果列表
		if (digits.empty()) {
			return combinations;
		}
		// 数字到字母的映射
		unordered_map<char, string> phoneMap{
			{'2', "abc"}, {'3', "def"}, {'4', "ghi"}, {'5', "jkl"},
			{'6', "mno"}, {'7', "pqrs"}, {'8', "tuv"}, {'9', "wxyz"}
		};

		string combination;
		// 开始回溯过程,从第0个索引和空的当前组合字符串开始
		backtrack(combinations, phoneMap, digits, 0, combination);
		return combinations;

	}

private:
	void backtrack(vector<string>& combinations, const unordered_map<char, string>& phoneMap,
		const string& digits, size_t index, string& combination) {
		// 如果当前索引等于数字字符串的长度,意味着已经处理完所有数字,当前组合完成
	   // 例如23 处理2的a后索引为1,然后处理3的d,索引为2,继续回溯的时候index=digits.length()
	   // 就把ad加入组合中
		if (index == digits.size()) {
			combinations.push_back(combination);
		}
		else {
			// 获取当前数字
			char digit = digits[index];
			// 在Map中找这个数字对应的字符串
			const string& letters = phoneMap.at(digit);
			// 遍历当前数字对应的字符串的所有字母
			for (char letter : letters) {
				// 将当前字母添加到组合中
				combination.push_back(letter);
				// 递归调用,处理下一个数字
				backtrack(combinations, phoneMap, digits, index + 1, combination);
				// 回溯:撤销上一步的选择(删除组合中的最后一个字母),尝试下一个可能的字母
				combination.pop_back();
			}
		}
	}
};

C版:

// 电话号码映射表
const char* map[10] = {
	"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
};

void backtrack(char* digits, char** combinations, char* temp, int index, int* returnSize, int digitsLen) {
	// 当前组合完成,复制到combinations中
	if (index == digitsLen) {
		combinations[*returnSize] = (char*)malloc((digitsLen + 1) * sizeof(char));
		strcpy(combinations[*returnSize], temp);
		(*returnSize)++;
		return;
	}

	// 获取当前数字对应的字母字符串,并遍历
	const char* letters = map[digits[index] - '0'];
	for (int i = 0; letters[i] != '\0'; i++) {
		temp[index] = letters[i]; // 将当前字母加入到当前组合中
		backtrack(digits, combinations, temp, index + 1, returnSize, digitsLen); // 递归处理下一个数字
		temp[index] = '\0'; // 回溯,撤销上一步的选择
	}
}

char** letterCombinations(char* digits, int* returnSize) {
	*returnSize = 0;
	int digitsLen = strlen(digits);
	if (digitsLen == 0) {
		return NULL;
	}

	// 预估结果的最大数量,以便分配足够的内存(最多4^长度)
	int maxCombinations = 1;
	for (int i = 0; i < digitsLen; i++) {
		if (digits[i] == '7' || digits[i] == '9') {
			maxCombinations *= 4;
		}
		else {
			maxCombinations *= 3;
		}
	}

	char** combinations = (char**)malloc(maxCombinations * sizeof(char*));
	char* temp = (char*)malloc((digitsLen + 1) * sizeof(char));
	temp[digitsLen] = '\0'; // 确保字符串正确结束

	backtrack(digits, combinations, temp, 0, returnSize, digitsLen);

	free(temp); // 释放临时字符串的内存
	return combinations;
}

解法二:迭代法

  • 描述:创建一个动态数组或列表以存储字符串组合,初始时仅包含一个空字符串。逐个处理输入字符串(电话号码)中的每个数字。对于每个数字,找到它在电话按键上对应的所有字母。对于当前数字对应的每个字母,将其添加到之前步骤生成的所有组合中,形成新的组合列表。使用新生成的组合列表替换原来的组合列表。完成所有输入数字的处理后,返回最终的组合列表。
  • 时间复杂度:O(3^M×4^N)。M 是对应三个字母的数字个数,N 是对应四个字母的数字个数。
  • 空间复杂度:O(3^M×4^N)。一共要生成 3^M×4^N个结果。
  • 参考代码:

java版:

class Solution {
    private static final String[] MAPPING = {
            "", // 0
            "", // 1
            "abc", // 2
            "def", // 3
            "ghi", // 4
            "jkl", // 5
            "mno", // 6
            "pqrs", // 7
            "tuv", // 8
            "wxyz" // 9
    };

    public List<String> letterCombinations(String digits) {
        List<String> result = new ArrayList<>();
        // 如果输入为空,则返回空的列表
        if (digits == null || digits.length() == 0) {
            return result;
        }
        // 初始化结果列表,添加一个空字符串以启动组合过程
        result.add("");

        for (char digit : digits.toCharArray()) {
            // 对于每个数字,找到对应的字母字符串,并与之前的结果组合
            // MAPPING[digit - '0']用于直接将数字字符转换为对应的数组索引,并访问该索引位置的元素
            result = combine(MAPPING[digit - '0'], result);
        }
        // 返回所有可能的字母组合
        return result;
    }

    // 为当前数字找到对应的所有字母,并与之前的结果进行组合
    private List<String> combine(String letters, List<String> prevResult) {
        List<String> newResult = new ArrayList<>();
        // 遍历当前数字对应的每个字母
        // 例如2对应abc
        for (char letter : letters.toCharArray()) {
            // 将当前字母与之前的每个组合结果相加
            // 就是""+a,""+b,""+c
            // 如果下一个是3的话就是def
            // a+d,b+d,c+d,a+e,b+e,c+e,a+f,b+f,c+f
            for (String str : prevResult) {
                newResult.add(str + letter);
            }
        }
        // 返回新的组合结果列表
        return newResult;
    }
}

C++版:

class Solution {
public:
	vector<string> letterCombinations(string digits) {
		if (digits.empty()) return {};
		// 数字到字母的映射
		vector<string> mapping = {
			 "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
		};
		// 初始化结果为包含一个空字符串的列表
		vector<string> result{ "" };
		//遍历给的数字字符串
		for (char digit : digits) {
			// 临时列表存储当前迭代的组合结果
			vector<string> temp;
			// 遍历当前数字对应的所有可能字母
			for (char letter : mapping[digit - '0']) {
				// 对之前的每个结果字符串
				for (const string& combination : result) {
					// 添加新字母形成新组合
					temp.push_back(combination + letter);
				}
			}
			// 更新结果列表为当前迭代的组合结果
			result.swap(temp);
		}

		return result;
	}
};

C版:

// 数字到字母的映射关系
char* mapping[] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

// 动态数组结构,用于存储字符串数组
typedef struct {
    char** elements; // 指向字符串数组的指针
    int size;        // 数组中字符串的数量
} DynamicArray;

// 初始化动态数组
void initDynamicArray(DynamicArray* arr) {
    arr->elements = NULL;
    arr->size = 0;
}

// 向动态数组添加字符串
void addElement(DynamicArray* arr, const char* str) {
    arr->size += 1;
    arr->elements = (char**)realloc(arr->elements, arr->size * sizeof(char*));
    arr->elements[arr->size - 1] = (char*)malloc(strlen(str) + 1);
    strcpy(arr->elements[arr->size - 1], str);
}

// 清理动态数组
void freeDynamicArray(DynamicArray* arr) {
    for (int i = 0; i < arr->size; i++) {
        free(arr->elements[i]);
    }
    free(arr->elements);
    arr->elements = NULL;
    arr->size = 0;
}

// 主要的迭代法实现
char** letterCombinations(char* digits, int* returnSize) {
        // 如果输入为空字符串,设置returnSize为0,并返回NULL
    if (digits == NULL || digits[0] == '\0') {
        *returnSize = 0;
        return NULL;
    }
    DynamicArray result;
    initDynamicArray(&result);
    // 开始时添加一个空字符串
    addElement(&result, "");

    for (int i = 0; digits[i] != '\0'; i++) {
        DynamicArray temp;
        initDynamicArray(&temp);

        char* letters = mapping[digits[i] - '0']; // 获取当前数字对应的字母
        for (int j = 0; letters[j] != '\0'; j++) {
            for (int k = 0; k < result.size; k++) {
                // 对于result中的每个元素,追加当前字母并添加到temp中
                char* newCombination = (char*)malloc(strlen(result.elements[k]) + 2);
                sprintf(newCombination, "%s%c", result.elements[k], letters[j]);
                addElement(&temp, newCombination);
                free(newCombination);
            }
        }

        // 使用temp更新result,并清理temp
        freeDynamicArray(&result);
        result = temp;
    }

    *returnSize = result.size;
    return result.elements;
}

解法三:队列

  • 描述:开始时,队列中只有一个空字符串,作为生成字母组合的基础。遍历输入的数字字符串,对于每个数字,按照映射关系找到对应的所有字母。对于当前数字对应的每个字母,从队列中取出前一步骤生成的所有组合(这些组合是基于之前所有数字的),然后将当前字母追加到每个组合后面,生成新的组合。将新生成的组合重新加入队列,为处理下一个数字做准备。当所有数字都处理完毕后,队列中存储的就是所有可能的字母组合。将这些组合从队列中取出,存储到结果数组中。
  • 时间复杂度:O(3^M×4^N)。M 是对应三个字母的数字个数,N 是对应四个字母的数字个数。
  • 空间复杂度:O(3^M×4^N)。一共要生成 3^M×4^N个结果。
  • 参考代码:

java版:

class Solution {
    public List<String> letterCombinations(String digits) {
        // 创建一个队列来存储所有可能的字母组合
        LinkedList<String> combinations = new LinkedList<>();
        // 如果输入的数字字符串为空,直接返回空的列表
        if (digits.isEmpty())
            return combinations;

        // 数字到字母的映射
        String[] mapping = new String[] {
                "0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
        };
        // 初始时,向队列中添加一个空字符串,作为构建组合的起点
        combinations.add("");
        // 遍历输入的每个数字,逐个处理
        for (int i = 0; i < digits.length(); i++) {
            // 将字符转换为数字,以便从映射表中获取对应的字母字符串
            int digit = Character.getNumericValue(digits.charAt(i));
            // 循环条件确保只处理当前数字生成的组合
            // 通过检查队列前端元素的长度与当前处理的数字索引i是否相等来控制循环。
            // 当长度不再等于i时,表示当前数字对应的所有可能字母已经处理完毕,循环结束,进入下一个数字的处理。
            while (combinations.peek().length() == i) {
                // 从队列前端移除元素,这个元素是当前处理步骤的基础组合
                String t = combinations.remove();
                // 遍历当前数字对应的所有可能字母
                for (char c : mapping[digit].toCharArray()) {
                    // 将当前字母添加到基础组合末尾,并将新组合加入队列末尾
                    combinations.add(t + c);
                }
            }
        }
        // 输入字符串处理完毕,队列中存储了所有可能的字母组合
        return combinations;
    }
}

C++版:

class Solution {
public:
	vector<string> letterCombinations(string digits) {
		vector<string> result;

		if (digits.empty()) return result;

		// 数字到字母的映射
		vector<string> mapping = { "0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };

		// 初始化队列并添加一个空字符串作为起始组合
		queue<string> q;
		q.push("");

		// 遍历每个数字字符
		for (char digit : digits) {
			int n = q.size();
			// 获取当前数字对应的所有可能字母
			string letters = mapping[digit - '0'];
			// 对于队列中的每个现有组合,尝试添加当前数字对应的每个字母
			for (int i = 0; i < n; i++) {
				// 获取并移除队列前端的组合
				string currentCombination = q.front();
				q.pop();
				// 遍历当前数字对应的每个字母,生成新组合并加入队列
				for (char letter : letters) {
					q.push(currentCombination + letter);
				}
			}
		}

		// 将队列中的所有组合移动到结果向量中
		while (!q.empty()) {
			result.push_back(q.front());
			q.pop();
		}

		return result;
	}
};

C版:

// 定义队列节点
typedef struct Node {
    char* combination; // 存储当前的字母组合
    struct Node* next; // 指向下一个节点
} Node;

// 定义队列结构
typedef struct {
    Node* front; // 队列头部
    Node* rear;  // 队列尾部
    int size;    // 队列中元素的数量
} Queue;

// 初始化队列
void initQueue(Queue* q) {
    q->front = q->rear = NULL;
    q->size = 0;
}

// 检查队列是否为空
int isEmpty(Queue* q) {
    return q->size == 0;
}

// 入队操作
void enqueue(Queue* q, const char* combination) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    int len = strlen(combination);
    newNode->combination = (char*)malloc(len + 1); // +1 for '\0'
    strcpy(newNode->combination, combination);
    newNode->next = NULL;
    
    if (q->rear == NULL) {
        q->front = q->rear = newNode;
    } else {
        q->rear->next = newNode;
        q->rear = newNode;
    }
    q->size++;
}

// 出队操作
char* dequeue(Queue* q) {
    if (isEmpty(q)) {
        return NULL;
    }
    Node* temp = q->front;
    char* combination = temp->combination;
    q->front = q->front->next;
    
    if (q->front == NULL) {
        q->rear = NULL;
    }
    free(temp); // 释放出队节点,但不释放字符串,字符串将返回给调用者
    q->size--;
    return combination;
}

// 电话号码到字母的映射
const char* mapping[] = {"0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

// 主函数,解决电话号码字母组合问题
char** letterCombinations(char* digits, int* returnSize) {

    // 如果输入为空字符串,设置returnSize为0,并返回NULL
    if (digits == NULL || digits[0] == '\0') {
        *returnSize = 0;
        return NULL;
    }
    
    Queue q;
    initQueue(&q);
    enqueue(&q, ""); // 初始入队一个空字符串,作为组合的起始点

    
    
    for (int i = 0; digits[i] != '\0'; i++) {
        int digit = digits[i] - '0';
        int n = q.size; // 记录当前队列大小,即这一轮需要处理的组合数量
        for (int j = 0; j < n; j++) {
            char* current = dequeue(&q);
            for (int k = 0; mapping[digit][k] != '\0'; k++) {
                int currLen = strlen(current);
                char* newCombination = (char*)malloc(currLen + 2); // +2: 1 for new char, 1 for '\0'
                strcpy(newCombination, current);
                newCombination[currLen] = mapping[digit][k];
                newCombination[currLen + 1] = '\0';
                enqueue(&q, newCombination);
                free(newCombination); // 避免内存泄漏
            }
            free(current); // 避免内存泄漏
        }
    }
    
    *returnSize = q.size;
    char** result = (char**)malloc(q.size * sizeof(char*));
    for (int i = 0; i < *returnSize; i++) {
        result[i] = dequeue(&q); // 将队列中的组合移动到结果数组中
    }
    return result;
}