14. Longest Common Prefix
Write a function to find the longest common prefix string amongst an array of strings.
If there is no common prefix, return an empty string "".
Example 1:
Input: strs = ["flower","flow","flight"] Output: "fl"
Example 2:
Input: strs = ["dog","racecar","car"] Output: "" Explanation: There is no common prefix among the input strings.
Constraints:
1 <= strs.length <= 2000 <= strs[i].length <= 200strs[i]consists of only lowercase English letters if it is non-empty.
// Horizontal Scanning (Quét ngang)
// lấy cái đầu tiên rồi so sánh, strs[i].find(prefix) !=0
// strs[i].compare(0, prefix.size(), prefix) != 0 để quét đầu chuỗi thay vì find quét toàn bộ chuỗi
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
if (strs.empty()) return "";
string prefix = strs[0];
for (int i=1; i< strs.size();i++){
while(strs[i].compare(0, prefix.size(), prefix) != 0){
prefix = prefix.substr(0, prefix.size()-1);
if (prefix.empty()) return "";
}
}
return prefix;
}
};
Dữ liệu đầu vào: strs = ["flower", "flow", "flight"]
Bước 1: Khởi tạo
-
if (strs.empty()): Sai, mảng có 3 phần tử. -
string prefix = strs[0];=>prefixbây giờ là "flower".
Bước 2: Vòng lặp for lần 1 (i = 1)
-
Đối tượng so sánh:
strs[1]là "flow". -
Vòng lặp
whilebắt đầu:-
Kiểm tra:
strs[1].compare(0, 6, "flower") != 0. -
Kết quả: Đúng ("flow" không giống "flower").
-
Thực thi:
prefix = prefix.substr(0, 5);=>prefixthành "flowe".
-
-
Tiếp tục
while:-
Kiểm tra:
strs[1].compare(0, 5, "flowe") != 0. -
Kết quả: Đúng ("flow" không giống "flowe").
-
Thực thi:
prefix = prefix.substr(0, 4);=>prefixthành "flow".
-
-
Tiếp tục
while:-
Kiểm tra:
strs[1].compare(0, 4, "flow") != 0. -
Kết quả: Sai ("flow" giống "flow", hàm
comparetrả về0). -
Dừng
while.
-
Bước 3: Vòng lặp for lần 2 (i = 2)
-
Đối tượng so sánh:
strs[2]là "flight". -
Vòng lặp
whilebắt đầu:-
Kiểm tra:
strs[2].compare(0, 4, "flow") != 0. -
Kết quả: Đúng ("flig" không giống "flow").
-
Thực thi:
prefix = prefix.substr(0, 3);=>prefixthành "flo".
-
-
Tiếp tục
while:-
Kiểm tra:
strs[2].compare(0, 3, "flo") != 0. -
Kết quả: Đúng ("fli" không giống "flo").
-
Thực thi:
prefix = prefix.substr(0, 2);=>prefixthành "fl".
-
-
Tiếp tục
while:-
Kiểm tra:
strs[2].compare(0, 2, "fl") != 0. -
Kết quả: Sai ("fl" giống "fl", hàm
comparetrả về0). -
Dừng
while.
-
Bước 4: Kết thúc
-
Vòng lặp
forkết thúc vì đã duyệt hết mảng. -
return prefix;=> Trả về "fl".
Phân tích nhanh về hàm compare
Trong đoạn code này, strs[i].compare(0, prefix.size(), prefix) cực kỳ an toàn vì:
-
Tham số 1 (0): Bắt đầu so sánh từ vị trí đầu tiên của
strs[i]. -
Tham số 2 (prefix.size()): Chỉ so sánh một đoạn có độ dài bằng đúng
prefix. -
Tham số 3 (prefix): Chuỗi mẫu để so sánh.
Lưu ý: Nếu prefix.size() lớn hơn độ dài của strs[i], hàm compare vẫn hoạt động bình thường và trả về giá trị khác 0, giúp vòng lặp while tiếp tục cắt ngắn prefix cho đến khi nó vừa khớp.
//Vertical Scanning (Quét dọc)
// so sánh ký tự thứ nhất của tất cả các chuỗi, rồi đến ký tự thứ hai...
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
if (strs.empty()) return "";
for (int i = 0; i < strs[0].length(); i++) {
char c = strs[0][i];
for (int j = 1; j < strs.size(); j++) {
// Nếu đi hết chiều dài của một chuỗi hoặc ký tự không khớp
if (i == strs[j].length() || strs[j][i] != c) {
return strs[0].substr(0, i);
}
}
}
return strs[0];
}
};
Chạy từng dòng code (Dry Run):
Khởi tạo: strs = ["flower", "flow", "flight"]
Vòng lặp 1: Xét cột i = 0
-
char c = strs[0][0]=>c = 'f'. -
So sánh với các chuỗi còn lại:
-
strs[1][0]là'f'(khớp). -
strs[2][0]là'f'(khớp).
-
-
Kết quả: Cột 0 hợp lệ. Tiếp tục.
Vòng lặp 2: Xét cột i = 1
-
char c = strs[0][1]=>c = 'l'. -
So sánh với các chuỗi còn lại:
-
strs[1][1]là'l'(khớp). -
strs[2][1]là'l'(khớp).
-
-
Kết quả: Cột 1 hợp lệ. Tiếp tục.
Vòng lặp 3: Xét cột i = 2
-
char c = strs[0][2]=>c = 'o'. -
So sánh với các chuỗi còn lại:
-
strs[1][2]là'o'(khớp). -
strs[2][2]là'i'(KHÔNG KHỚP!).
-
-
Hành động: Gặp điều kiện
strs[2][2] != 'o', hàm lập tức thực hiệnreturn strs[0].substr(0, 2).
Kết quả cuối cùng: "fl"
1. Tại sao phải kiểm tra i == strs[j].length()?
Trong vòng lặp, biến i đại diện cho chỉ số ký tự hiện tại mà chúng ta đang xét. Nếu i đã bằng với độ dài của một chuỗi nào đó trong mảng, điều đó có nghĩa là chúng ta đã duyệt hết sạch ký tự của chuỗi ngắn nhất đó rồi.
-
Tiền tố chung không thể dài hơn chuỗi ngắn nhất trong mảng.
-
Nếu không có điều kiện này, khi
ităng lên, bạn sẽ cố gắng truy cập vàostrs[j][i]– một vị trí không tồn tại, dẫn đến lỗi Undefined Behavior hoặc Crash chương trình.
2. Ví dụ minh họa
Giả sử bạn có: strs = ["flow", "f"]
Bước 1: Xét cột i = 0
-
c = strs[0][0]là'f'. -
Xét
strs[1]:i (0)chưa bằngstrs[1].length() (1). -
strs[1][0]là'f', khớp vớic. -
Tiếp tục.
Bước 2: Xét cột i = 1
-
c = strs[0][1]là'l'. -
Xét
strs[1]: Lúc nàyi (1) == strs[1].length() (1). -
Điều kiện đúng! Hàm ngay lập tức
return strs[0].substr(0, 1), kết quả là"f".
3. Thứ tự của điều kiện (Short-circuit evaluation)
Trong C++, toán tử || (OR) hoạt động theo cơ chế ngắt mạch:
-
Nó sẽ kiểm tra
i == strs[j].length()trước. -
Nếu điều kiện này đúng, nó dừng lại luôn và thực hiện lệnh bên trong
if, không thèm kiểm trastrs[j][i] != cnữa. -
Điều này giúp bảo vệ chương trình vì nếu
iđã quá dài, việc kiểm trastrs[j][i]sẽ gây lỗi.
Tóm lại:
-
i == strs[j].length(): Kiểm tra xem chuỗi hiện tại đã hết ký tự chưa. => chạyi == strs[j].length() trước ví đặt trước. -
strs[j][i] != c: Kiểm tra xem ký tự tại vị tríicó khác biệt không.
Chỉ cần một trong hai điều kiện này đúng, nghĩa là tiền tố chung đã kết thúc tại vị trí i.
// Sắp xếp (Sorting)
// so sánh chuỗi đầu tiên và chuỗi cuối cùng
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
if (strs.empty()) return "";
// 1. Sắp xếp mảng theo thứ tự từ điển
sort(strs.begin(), strs.end());
string first = strs[0];
string last = strs.back();
string result = "";
// 2. Chỉ cần so sánh chuỗi đầu và chuỗi cuối
for (int i = 0; i < min(first.size(), last.size()); i++) {
if (first[i] != last[i]) {
return result;
}
result += first[i];
}
return result;
}
};
Các bước thực thi:
1. Khởi tạo:
-
Mảng ban đầu:
["flower", "flow", "flight"]
2. Sắp xếp mảng (sort(strs.begin(), strs.end())):
-
Sau khi sắp xếp theo thứ tự từ điển, mảng trở thành:
["flight", "flow", "flower"](Giải thích: "flight" đứng đầu vì 'i' đứng trước 'o', "flow" đứng trước "flower" vì nó ngắn hơn dù các ký tự đầu giống nhau).
3. Xác định first và last:
-
first = "flight" -
last = "flower" -
result = ""(Chuỗi rỗng để chứa kết quả)
4. Vòng lặp so sánh first và last:
Ta sẽ so sánh từng ký tự tại chỉ số $i$:
-
Lượt 1 (i = 0):
-
first[0]là'f',last[0]là'f'. -
Giống nhau =>
result = "f".
-
-
Lượt 2 (i = 1):
-
first[1]là'l',last[1]là'l'. -
Giống nhau =>
result = "fl".
-
-
Lượt 3 (i = 2):
-
first[2]là'i',last[2]là'o'. -
Khác nhau! => Dừng vòng lặp và trả về
result.
-
Kết quả cuối cùng: "fl"