Để giúp làm rõ hành vi của Array#sort
và bộ so sánh của nó, hãy xem xét loại chèn ngây thơ này được dạy trong các khóa học lập trình bắt đầu:
const sort = arr => {
for (let i = 1; i < arr.length; i++) {
for (let j = i; j && arr[j-1] > arr[j]; j--) {
[arr[j], arr[j-1]] = [arr[j-1], arr[j]];
}
}
};
const array = [3, 0, 4, 5, 2, 2, 2, 1, 2, 2, 0];
sort(array);
console.log("" + array);
Bỏ qua lựa chọn sắp xếp chèn như các thuật toán, tập trung vào các mã hóa cứng so sánh: arr[j-1] > arr[j]
. Điều này có hai vấn đề liên quan đến cuộc thảo luận:
- Các
>
nhà điều hành được gọi vào cặp phần tử mảng nhưng nhiều điều bạn có thể muốn sắp xếp như các đối tượng không đáp ứng với >
một cách hợp lý (cùng sẽ là sự thật nếu chúng ta sử dụng -
).
- Ngay cả khi bạn đang làm việc với các con số, đôi khi bạn muốn một số sắp xếp khác hơn là sắp xếp tăng dần đã được đưa ra ở đây.
Chúng tôi có thể khắc phục những sự cố này bằng cách thêm một comparefn
đối số mà bạn đã quen thuộc:
const sort = (arr, comparefn) => {
for (let i = 1; i < arr.length; i++) {
for (let j = i; j && comparefn(arr[j-1], arr[j]) > 0; j--) {
[arr[j], arr[j-1]] = [arr[j-1], arr[j]];
}
}
};
const array = [3, 0, 4, 5, 2, 2, 2, 1, 2, 2, 0];
sort(array, (a, b) => a - b);
console.log("" + array);
sort(array, (a, b) => b - a);
console.log("" + array);
const objArray = [{id: "c"}, {id: "a"}, {id: "d"}, {id: "b"}];
sort(objArray, (a, b) => a.id.localeCompare(b.id));
console.log(JSON.stringify(objArray, null, 2));
Giờ đây, thói quen sắp xếp ngây thơ đã được khái quát hóa. Bạn có thể biết chính xác khi nào gọi lại này được gọi, trả lời nhóm mối quan tâm đầu tiên của bạn:
Hàm gọi lại sắp xếp mảng có được gọi nhiều lần trong quá trình sắp xếp không? Nếu vậy, tôi muốn biết hai số nào được chuyển vào hàm mỗi lần
Chạy đoạn mã dưới đây cho thấy rằng, có, hàm được gọi nhiều lần và bạn có thể sử dụng console.log
để xem những số nào đã được chuyển vào:
const sort = (arr, comparefn) => {
for (let i = 1; i < arr.length; i++) {
for (let j = i; j && comparefn(arr[j-1], arr[j]) > 0; j--) {
[arr[j], arr[j-1]] = [arr[j-1], arr[j]];
}
}
};
console.log("on our version:");
const array = [3, 0, 4, 5];
sort(array, (a, b) => console.log(a, b) || (a - b));
console.log("" + array);
console.log("on the builtin:");
console.log("" +
[3, 0, 4, 5].sort((a, b) => console.log(a, b) || (a - b))
);
Bạn hỏi:
Hai bộ số sau đó được sắp xếp như thế nào trong mối quan hệ với nhau?
Nói chính xác với thuật ngữ, a
và b
không phải là tập hợp số - chúng là các đối tượng trong mảng (trong ví dụ của bạn, chúng là số).
Sự thật là, chúng được sắp xếp như thế nào không quan trọng vì nó phụ thuộc vào việc triển khai. Nếu tôi đã sử dụng một thuật toán sắp xếp khác với sắp xếp chèn, bộ so sánh có thể sẽ được gọi trên các cặp số khác nhau, nhưng ở cuối lệnh gọi sắp xếp, bất biến quan trọng đối với lập trình viên JS là mảng kết quả được sắp xếp theo bộ so sánh, giả sử bộ so sánh trả về các giá trị tuân theo hợp đồng bạn đã nêu (<0 khi a < b
, 0 khi a === b
và> 0 khi a > b
).
Cũng theo nghĩa là tôi có quyền tự do thay đổi cách triển khai sắp xếp của mình miễn là tôi không vi phạm đặc điểm kỹ thuật của mình, việc triển khai ECMAScript có thể tự do lựa chọn cách triển khai sắp xếp trong giới hạn của đặc tả ngôn ngữ , do đó Array#sort
có thể sẽ tạo ra các lệnh gọi so sánh khác nhau trên các động cơ khác nhau. Người ta sẽ không viết mã trong đó logic dựa vào một số chuỗi so sánh cụ thể (và ngay từ đầu trình so sánh cũng không nên tạo ra các tác dụng phụ).
Ví dụ: động cơ V8 (tại thời điểm viết bài) gọi Timsort khi mảng lớn hơn một số phần tử được tính toán trước và sử dụng sắp xếp chèn nhị phân cho các phần mảng nhỏ. Tuy nhiên, nó đã từng sử dụng quicksort không ổn định và có thể sẽ đưa ra một chuỗi đối số và lệnh gọi khác nhau tới trình so sánh.
Vì các triển khai sắp xếp khác nhau sử dụng giá trị trả về của hàm so sánh khác nhau, điều này có thể dẫn đến hành vi đáng ngạc nhiên khi trình so sánh không tuân thủ hợp đồng. Xem chủ đề này để biết ví dụ.