Sắp Xếp Chẵn Trái Lẻ Phải
Đề bài
Cho mảng số tự nhiên có $n$ phần tử. Viết hàm sắp xếp các phần tử có giá trị chẵn trong mảng sang phần bên trái (từ nhỏ đến lớn) và các phần tử có giá trị lẻ sang phần bên phải của mảng (từ nhỏ đến lớn).
Ví dụ: $\{2, 7, 6, 10, 9, 3\} \Rightarrow \{2, 6, 10, 3, 7, 9\}$.
(Đề gốc chỉ yêu cầu viết hàm, không nói định dạng vào/ra. Để chạy thử và kiểm tra, trong bài này mình quy ước: dòng đầu là $n$ $(1 \le n \le 10^5)$, dòng sau là $n$ số tự nhiên $a_i$ $(0 \le a_i \le 10^9)$; in ra mảng sau khi sắp xếp trên một dòng, các số cách nhau một khoảng trắng.)
6
2 7 6 10 9 32 6 10 3 7 9Lời mở đầu
Trong quá trình đi comment dạo mình thấy bài tập này cũng hay nên chia sẻ lên đây để anh em cùng xem và thử sức nhé. Đây là câu hỏi của một bạn newbie đang ôn thi học phần Cấu trúc dữ liệu và Giải thuật: bạn ấy tính dùng cách tách mảng, nhưng vì là học phần CTDL và GT nên muốn dùng một thuật toán "xịn" hơn, kiểu như quicksort. Mình sẽ đưa ra cả hai hướng theo cách làm của mình để anh em cùng tham khảo và thảo luận hen. Hy vọng mang đến giá trị cho anh em 😇.
Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì link bài đây nhé:
Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.
Nộp bài trên CLUE OJGiải thích ví dụ: mảng $\{2, 7, 6, 10, 9, 3\}$ có các số chẵn là $2, 6, 10$ và các số lẻ là $7, 9, 3$. Dồn các số chẵn về bên trái, sắp tăng dần: $2, 6, 10$; dồn các số lẻ về bên phải, sắp tăng dần: $3, 7, 9$ ⇒ kết quả $\{2, 6, 10, 3, 7, 9\}$. Lưu ý số $0$ cũng là số tự nhiên chẵn nhé.
Lộ trình lời giải, bấm vào từng phần để nhảy tới:
Cách 1Tách ra 2 mảng chẵn, lẻ – O(N log N)
Nếu đề bài chỉ là như vậy, không có yêu cầu ràng buộc gì thêm và đang trong ngữ cảnh phòng thi giới hạn thời gian thì chúng ta hãy chọn cách dễ dàng nhất:
- Tạo một mảng mới chứa các số chẵn, một mảng mới chứa các số lẻ.
- Sắp xếp 2 mảng đó tăng dần.
- Gán lại giá trị vào mảng ban đầu: các phần tử của mảng chẵn gán vào trước, tiếp đến mảng lẻ gán vào sau.
Như thế mảng gốc ban đầu sẽ có đúng trật tự như đề bài mong muốn. Cách này quá đơn giản rồi, code mẫu đây cho anh em nào mới học tham khảo:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n), chan, le;
for(int i = 0; i < n; i++) {
cin >> a[i];
if(a[i] % 2 == 0) {
chan.push_back(a[i]);
}
else {
le.push_back(a[i]);
}
}
sort(chan.begin(), chan.end());
sort(le.begin(), le.end());
int k = 0;
for(int i = 0; i < (int)chan.size(); i++) {
a[k++] = chan[i];
}
for(int i = 0; i < (int)le.size(); i++) {
a[k++] = le[i];
}
for(int i = 0; i < n; i++) {
cout << a[i] << " ";
}
return 0;
}
n = int(input())
a = list(map(int, input().split()))
chan = sorted(x for x in a if x % 2 == 0)
le = sorted(x for x in a if x % 2 != 0)
a = chan + le
print(*a)
- Không gian: thêm 2 mảng phụ, tổng cộng $N$ phần tử ⇒ $O(N)$.
- Thời gian: tách mảng $O(N)$, sắp xếp $O(N \log N)$ ⇒ $O(N \log N)$.
Cách 2Hai con trỏ left, right ngay trên mảng gốc – O(N log N)
Nhưng nếu đề bài có thêm ràng buộc là không được dùng thêm bộ nhớ bên ngoài, nói rõ hơn là không được tạo thêm các mảng trung gian mà chỉ được xử lý trên mảng gốc ban đầu; hoặc đề không ràng buộc nhưng chúng ta không có áp lực phòng thi gì cả, thì hãy thử sức bản thân theo hướng này. Nó hoàn toàn có thể giải quyết được mà không cần tạo ra các mảng phụ, chỉ xử lý trên duy nhất mảng gốc ban đầu là được.
Tư tưởng như sau:
- Tạo 2 biến
left,right:leftlà chỉ số đầu tiên,rightlà chỉ số cuối cùng của mảng.leftchạy từ đầu về cuối,rightchạy từ cuối về đầu.leftđại diện cho phần mảng số chẵn,rightđại diện cho phần mảng số lẻ. - Nếu phần tử tại mỗi biến đã đứng đúng chỗ (chẵn ở
left, lẻ ởright) thì biến đó bỏ qua, xét phần tử tiếp theo. Nếu phát hiện phần tử đứng sai chỗ thì biến đó đứng lại tại đó. - Khi cả hai đều đang đứng tại một phần tử sai chỗ (bên trái là số lẻ, bên phải là số chẵn) thì đổi chỗ 2 phần tử đó cho về đúng vị trí.
- Quy trình cứ thế lặp lại đến khi
left >= rightthì dừng, vì lúc này mỗi thằng đều đã đi hết phạm vi được phép của mình.
Giờ đây mảng gốc đã có các số chẵn dồn hết về đầu, số lẻ dồn hết về cuối, ta chỉ việc sắp xếp tăng dần trong phạm vi của mỗi đứa tụi nó thôi.
left tăng 1, hoặc right giảm 1, hoặc đổi chỗ (sau khi đổi thì a[left] chẵn nên lần lặp kế tiếp left sẽ tăng). Vì vậy vòng lặp dừng đúng lúc left == right. Phần tử tại vị trí gặp nhau này chưa được ai kiểm tra: nếu nó chẵn thì nó thuộc phần chẵn ⇒ phần chẵn là a[0..left], gồm left + 1 phần tử; nếu lẻ thì phần chẵn là a[0..left-1], gồm left phần tử. Đó chính là ý nghĩa của biến x trong code: số phần tử chẵn $= left + x$.sort trong thư viện algorithm cho nhanh; nếu anh em tự code hàm sắp xếp (quicksort, mergesort…) thì cứ thay vào, miễn là cho phép sắp xếp trong một phạm vi cụ thể của mảng mà ta chủ động truyền tham số vào được.Minh hoạ hai con trỏ
Em nhập mảng rồi bấm Tiếp để xem left, right chạy, đứng lại và đổi chỗ như thế nào:
Source code để các bạn tham khảo nếu làm theo hướng này (hàm Solve giữ nguyên như mình viết, chỉ thêm phần đọc dữ liệu cho dễ chạy thử):
#include <iostream>
#include <algorithm>
using namespace std;
int a[100005];
void Solve(int a[], int n) {
int left = 0;
int right = n - 1;
while(left < right) {
if(a[left] % 2 == 0) {
left++;
continue;
}
if(a[right] % 2 != 0) {
right--;
continue;
}
swap(a[left], a[right]);
}
int x = 0;
if(a[left] % 2 == 0) {
x = 1;
}
sort(a, a + left + x);
sort(a + left + x, a + n);
}
int main() {
int n;
cin >> n;
for(int i = 0; i < n; i++) {
cin >> a[i];
}
Solve(a, n);
for(int i = 0; i < n; i++) {
cout << a[i] << " ";
}
return 0;
}
def solve(a, n):
left, right = 0, n - 1
while left < right:
if a[left] % 2 == 0:
left += 1
continue
if a[right] % 2 != 0:
right -= 1
continue
a[left], a[right] = a[right], a[left]
x = 1 if a[left] % 2 == 0 else 0
a[:left + x] = sorted(a[:left + x])
a[left + x:] = sorted(a[left + x:])
n = int(input())
a = list(map(int, input().split()))
solve(a, n)
print(*a)
Đánh giá độ phức tạp của cách làm này:
- Không gian: chỉ dùng vài biến
left,right,x, không tạo mảng phụ nào ⇒ $O(1)$ bộ nhớ phụ (không tính mảng đầu vào). - Thời gian: phần hai con trỏ mỗi bước đều thu hẹp khoảng $[left, right]$ (hoặc đổi chỗ rồi bước kế tiếp thu hẹp) ⇒ $O(N)$; phần sắp xếp $O(N \log N)$ ⇒ tổng $O(N \log N)$.
“ ” và dấu — (thay vì --) sẽ bị sai khi copy vào máy tính. Code trong bài này đã được sửa lại chuẩn rồi, anh em cứ yên tâm dùng.- Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
- Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
- Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
- Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
- Dễ nản giữa chừng khi không có ai theo sát.
- Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
- Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
- Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
- Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.
Nhận xét
Đăng nhận xét