So Sánh Chuỗi
Để so sánh chuỗi trong MQL5, bạn có thể sử dụng các toán tử quan hệ chuẩn, cụ thể là ==
, !=
, >
, <
. Tất cả các toán tử này thực hiện so sánh từng ký tự một và có phân biệt chữ hoa chữ thường.
Mỗi ký tự có một mã Unicode, là một số nguyên kiểu ushort
. Theo đó, trước tiên mã của ký tự đầu tiên của hai chuỗi được so sánh, sau đó là mã của ký tự thứ hai, và tiếp tục như vậy cho đến khi gặp sự không khớp đầu tiên hoặc đến cuối của một trong hai chuỗi.
Ví dụ, chuỗi "ABC" nhỏ hơn "abc", vì mã của các chữ cái in hoa trong bảng ký tự thấp hơn mã của các chữ cái thường tương ứng (ngay ở ký tự đầu tiên, ta đã có "A" < "a"). Nếu các chuỗi có các ký tự khớp nhau ở đầu, nhưng một chuỗi dài hơn chuỗi kia, thì chuỗi dài hơn được coi là lớn hơn ("ABCD" > "ABC").
Các mối quan hệ chuỗi như vậy tạo thành thứ tự từ vựng (lexicographic order). Khi chuỗi "A" nhỏ hơn chuỗi "B" ("A" < "B"), người ta nói rằng "A" đứng trước "B".
Để làm quen với mã ký tự, bạn có thể sử dụng ứng dụng tiêu chuẩn của Windows "Bảng Ký Tự". Trong đó, các ký tự được sắp xếp theo thứ tự mã tăng dần. Ngoài bảng Unicode chung, bao gồm nhiều ngôn ngữ quốc gia, còn có các bảng mã: các bảng chuẩn ANSI với mã ký tự một byte — chúng khác nhau cho mỗi ngôn ngữ hoặc nhóm ngôn ngữ. Chúng ta sẽ khám phá vấn đề này chi tiết hơn trong phần Làm việc với ký tự và bảng mã.
Phần đầu của bảng ký tự với mã từ 0 đến 127 là giống nhau cho tất cả các ngôn ngữ. Phần này được hiển thị trong bảng sau.
Bảng mã ký tự ASCII
Để lấy mã ký tự, lấy chữ số thập lục phân ở bên trái (số dòng mà ký tự nằm trong đó) và cộng với số ở trên cùng (số cột mà ký tự nằm trong đó): kết quả là một số thập lục phân. Ví dụ, đối với '!' có 2 ở bên trái và 1 ở trên cùng, nghĩa là mã ký tự là 0x21, hoặc 33 trong hệ thập phân.
Các mã lên đến 32 là mã điều khiển. Trong số đó, bạn có thể tìm thấy, cụ thể là, tab (mã 0x9), xuống dòng (mã 0xA), và trở về đầu dòng (mã 0xD).
Một cặp ký tự 0xD 0xA liền kề nhau được sử dụng trong các tệp văn bản Windows để ngắt sang dòng mới. Chúng ta đã làm quen với các ký tự chữ MQL5 tương ứng trong phần Kiểu ký tự: 0xA có thể được ký hiệu là \n
và 0xD là \r
. Tab 0x9 cũng có biểu diễn riêng: \t
.
API MQL5 cung cấp hàm StringCompare
, cho phép tắt tính năng phân biệt chữ hoa chữ thường khi so sánh chuỗi.
int StringCompare(const string &string1, const string &string2, const bool case_sensitive = true)
Hàm này so sánh hai chuỗi và trả về một trong ba giá trị: +1 nếu chuỗi đầu tiên "lớn hơn" chuỗi thứ hai; 0 nếu hai chuỗi "bằng nhau"; -1 nếu chuỗi đầu tiên "nhỏ hơn" chuỗi thứ hai. Khái niệm "lớn hơn", "nhỏ hơn" và "bằng nhau" phụ thuộc vào tham số case_sensitive
.
Khi tham số case_sensitive
bằng true
(mặc định), so sánh có phân biệt chữ hoa chữ thường, với các chữ cái in hoa được coi là lớn hơn các chữ cái thường tương tự. Điều này ngược lại với thứ tự từ vựng chuẩn theo mã ký tự.
Khi có phân biệt chữ hoa chữ thường, hàm
StringCompare
sử dụng thứ tự chữ cái in hoa và thường khác với thứ tự từ vựng. Ví dụ, chúng ta biết rằng quan hệ "A" < "a" là đúng, trong đó toán tử '<' dựa vào mã ký tự. Do đó, các từ in hoa nên xuất hiện trong từ điển giả định (mảng) trước các từ có cùng chữ cái thường. Tuy nhiên, khi so sánh "A" và "a" bằng hàmStringCompare("A", "a")
, chúng ta nhận được +1, nghĩa là "A" lớn hơn "a". Vì vậy, trong từ điển đã sắp xếp, các từ bắt đầu bằng chữ cái thường sẽ đứng trước, và chỉ sau đó là các từ có chữ cái in hoa.
Nói cách khác, hàm này xếp hạng các chuỗi theo thứ tự bảng chữ cái. Ngoài ra, trong chế độ phân biệt chữ hoa chữ thường, áp dụng một quy tắc bổ sung: nếu có các chuỗi chỉ khác nhau về chữ hoa chữ thường, những chuỗi có chữ cái in hoa sẽ đứng sau các chuỗi tương ứng có chữ cái thường (tại cùng vị trí trong từ).
Nếu tham số case_sensitive
bằng false
, các chữ cái không phân biệt chữ hoa chữ thường, vì vậy chuỗi "A" và "a" được coi là bằng nhau, và hàm trả về 0.
Bạn có thể kiểm tra các kết quả so sánh khác nhau bằng hàm StringCompare
và bằng toán tử sử dụng script StringCompare.mq5
.
void OnStart()
{
PRT(StringCompare("A", "a")); // 1, nghĩa là "A" > "a" (!)
PRT(StringCompare("A", "a", false)); // 0, nghĩa là "A" == "a"
PRT("A" > "a"); // false, "A" < "a"
PRT(StringCompare("x", "y")); // -1, nghĩa là "x" < "y"
PRT("x" > "y"); // false, "x" < "y"
...
}
2
3
4
5
6
7
8
9
10
Trong phần Hàm Templates, chúng ta đã tạo một thuật toán quicksort mẫu. Hãy biến nó thành một lớp mẫu và sử dụng nó cho một số tùy chọn sắp xếp: sử dụng toán tử so sánh, cũng như sử dụng hàm StringCompare
với cả hai chế độ có và không phân biệt chữ hoa chữ thường. Hãy đặt lớp mới QuickSortT
vào tệp tiêu đề QuickSortT.mqh
và kết nối nó với script kiểm tra StringCompare.mq5
.
API sắp xếp hầu như không thay đổi.
template<typename T>
class QuickSortT
{
public:
void Swap(T &array[], const int i, const int j)
{
...
}
virtual int Compare(T &a, T &b)
{
return a > b ? +1 : (a < b ? -1 : 0);
}
void QuickSort(T &array[], const int start = 0, int end = INT_MAX)
{
...
for(int i = start; i <= end; i++)
{
//if(!(array[i] > array[end]))
if(Compare(array[i], array[end]) <= 0)
{
Swap(array, i, pivot++);
}
}
...
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Sự khác biệt chính là chúng ta đã thêm một phương thức ảo Compare
, mặc định chứa so sánh sử dụng toán tử '>' và '<', và trả về +1, -1, hoặc 0 giống như StringCompare
. Phương thức Compare
giờ được sử dụng trong phương thức QuickSort
thay vì so sánh đơn giản và phải được ghi đè trong các lớp con để sử dụng hàm StringCompare
hoặc bất kỳ cách so sánh nào khác.
Cụ thể, trong tệp StringCompare.mq5
, chúng ta triển khai lớp "comparator" sau dẫn xuất từ QuickSortT<string>
:
class SortingStringCompare : public QuickSortT<string>
{
const bool caseEnabled;
public:
SortingStringCompare(const bool sensitivity = true) :
caseEnabled(sensitivity) { }
virtual int Compare(string &a, string &b) override
{
return StringCompare(a, b, caseEnabled);
}
};
2
3
4
5
6
7
8
9
10
11
12
Hàm tạo nhận 1 tham số, chỉ định dấu so sánh chuỗi có tính đến (true
) hoặc bỏ qua (false
) chữ hoa chữ thường. Việc so sánh chuỗi được thực hiện trong phương thức ảo được định nghĩa lại Compare
, gọi hàm StringCompare
với các đối số và cài đặt đã cho.
Để kiểm tra sắp xếp, chúng ta cần một tập hợp chuỗi kết hợp chữ cái in hoa và thường. Chúng ta có thể tự tạo nó: chỉ cần phát triển một lớp thực hiện hoán vị (có lặp lại) của các ký tự từ một tập hợp đã xác định trước (bảng chữ cái) cho một độ dài tập hợp nhất định (chuỗi). Ví dụ, bạn có thể giới hạn ở bảng chữ cái nhỏ "abcABC", tức là ba chữ cái tiếng Anh đầu tiên ở cả hai dạng, và tạo tất cả các chuỗi 2 ký tự có thể từ chúng.
Lớp PermutationGenerator
được cung cấp trong tệp PermutationGenerator.mqh
và để lại cho việc tự nghiên cứu. Ở đây chúng ta chỉ trình bày giao diện công khai của nó.
class PermutationGenerator
{
public:
struct Result
{
int indices[]; // chỉ số của các phần tử ở mỗi vị trí của tập hợp, tức là
}; // ví dụ, số của các chữ cái trong "bảng chữ cái" ở mỗi vị trí của chuỗi
PermutationGenerator(const int length, const int elements);
SimpleArray<Result> *run();
};
2
3
4
5
6
7
8
9
10
Khi tạo một đối tượng generator, bạn phải chỉ định độ dài của các tập hợp được tạo length
(trong trường hợp của chúng ta, đây sẽ là độ dài của chuỗi, tức là 2) và số lượng phần tử khác nhau mà từ đó các tập hợp sẽ được tạo thành (trong trường hợp của chúng ta, đây là số lượng chữ cái duy nhất, tức là 6). Với dữ liệu đầu vào như vậy, sẽ nhận được 6 * 6 = 36 biến thể của chuỗi.
Quá trình này được thực hiện bởi phương thức run
. Một lớp mẫu được sử dụng để trả về một mảng với kết quả SimpleArray
, mà chúng ta đã thảo luận trong phần Phương Thức Templates. Trong trường hợp này, nó được tham số hóa bởi kiểu cấu trúc result
.
Việc gọi generator và tạo thực tế các chuỗi theo mảng hoán vị nhận được từ nó (dưới dạng chỉ số chữ cái ở mỗi vị trí cho tất cả các chuỗi có thể) được thực hiện trong hàm trợ giúp GenerateStringList
.
void GenerateStringList(const string symbols, const int len, string &result[])
{
const int n = StringLen(symbols); // độ dài bảng chữ cái, ký tự duy nhất
PermutationGenerator g(len, n);
SimpleArray<PermutationGenerator::Result> *r = g.run();
ArrayResize(result, r.size());
// lặp qua tất cả các hoán vị ký tự nhận được
for(int i = 0; i < r.size(); ++i)
{
string element;
// lặp qua tất cả các ký tự trong chuỗi
for(int j = 0; j < len; ++j)
{
// thêm một chữ cái từ bảng chữ cái (theo chỉ số của nó) vào chuỗi
element += ShortToString(symbols[r[i].indices[j]]);
}
result[i] = element;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Ở đây chúng ta sử dụng một số hàm vẫn chưa quen thuộc với chúng ta (ArrayResize
, ShortToString
), nhưng chúng ta sẽ sớm tìm hiểu về chúng. Hiện tại, chúng ta chỉ cần biết rằng hàm ShortToString
trả về một chuỗi gồm một ký tự duy nhất dựa trên mã ký tự kiểu ushort
. Sử dụng toán tử '+=', chúng ta nối mỗi chuỗi kết quả từ các chuỗi một ký tự như vậy. Hãy nhớ rằng toán tử [] được định nghĩa cho chuỗi, vì vậy biểu thức symbols[k]
sẽ trả về ký tự thứ k
của chuỗi symbols
. Tất nhiên, k
có thể là một biểu thức số nguyên, và ở đây r[i].indices[j]
đang tham chiếu đến phần tử thứ i
của mảng r
từ đó chỉ số của ký tự "bảng chữ cái" được đọc cho vị trí thứ j
của chuỗi.
Mỗi chuỗi nhận được được lưu trữ trong mảng-tham số result
.
Hãy áp dụng thông tin này trong hàm OnStart
.
void OnStart()
{
...
string messages[];
GenerateStringList("abcABC", 2, messages);
Print("Original data[", ArraySize(messages), "]:");
ArrayPrint(messages);
Print("Default case-sensitive sorting:");
QuickSortT<string> sorting;
sorting.QuickSort(messages);
ArrayPrint(messages);
Print("StringCompare case-insensitive sorting:");
SortingStringCompare caseOff(false);
caseOff.QuickSort(messages);
ArrayPrint(messages);
Print("StringCompare case-sensitive sorting:");
SortingStringCompare caseOn(true);
caseOn.QuickSort(messages);
ArrayPrint(messages);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Script trước tiên lấy tất cả các tùy chọn chuỗi vào mảng messages
và sau đó sắp xếp nó theo 3 chế độ: sử dụng toán tử so sánh tích hợp, sử dụng hàm StringCompare
ở chế độ không phân biệt chữ hoa chữ thường và sử dụng cùng hàm ở chế độ phân biệt chữ hoa chữ thường.
Chúng ta sẽ nhận được đầu ra nhật ký sau:
Original data[36]:
[ 0] "aa" "ab" "ac" "aA" "aB" "aC" "ba" "bb" "bc" "bA" "bB" "bC" "ca" "cb" "cc" "cA" "cB" "cC"
[18] "Aa" "Ab" "Ac" "AA" "AB" "AC" "Ba" "Bb" "Bc" "BA" "BB" "BC" "Ca" "Cb" "Cc" "CA" "CB" "CC"
Default case-sensitive sorting:
[ 0] "AA" "AB" "AC" "Aa" "Ab" "Ac" "BA" "BB" "BC" "Ba" "Bb" "Bc" "CA" "CB" "CC" "Ca" "Cb" "Cc"
[18] "aA" "aB" "aC" "aa" "ab" "ac" "bA" "bB" "bC" "ba" "bb" "bc" "cA" "cB" "cC" "ca" "cb" "cc"
StringCompare case-insensitive sorting:
[ 0] "AA" "Aa" "aA" "aa" "AB" "aB" "Ab" "ab" "aC" "AC" "Ac" "ac" "BA" "Ba" "bA" "ba" "BB" "bB"
[18] "Bb" "bb" "bC" "BC" "Bc" "bc" "CA" "Ca" "cA" "ca" "CB" "cB" "Cb" "cb" "cC" "CC" "Cc" "cc"
StringCompare case-sensitive sorting:
[ 0] "aa" "aA" "Aa" "AA" "ab" "aB" "Ab" "AB" "ac" "aC" "Ac" "AC" "ba" "bA" "Ba" "BA" "bb" "bB"
[18] "Bb" "BB" "bc" "bC" "Bc" "BC" "ca" "cA" "Ca" "CA" "cb" "cB" "Cb" "CB" "cc" "cC" "Cc" "CC"
2
3
4
5
6
7
8
9
10
11
12
Đầu ra cho thấy sự khác biệt giữa ba chế độ này.