Phân tích thuật toán• Nhằm xác định thời gian chạy độ phức tạp của thuật toán dưới dạng một hàm f của kích thước đầu vào n − VD: Thời gian tìm tuần tự một phần tử x trong một dãy n phầ
Trang 1Phân tích thuật toán
Nguyễn Mạnh Hiển
hiennm@tlu.edu.vn
Trang 31 Phân tích thuật toán là gì?
Trang 4Phân tích thuật toán
• Nhằm xác định thời gian chạy (độ phức tạp) của thuật
toán dưới dạng một hàm f của kích thước đầu vào n
− VD: Thời gian tìm tuần tự một phần tử x trong một dãy
n phần tử là f(n) = n
• Đơn vị thời gian:
− Không phải là giờ, phút, giây
− Mà là thao tác cơ bản, VD: cộng, nhân, so sánh
− Mỗi thao tác cơ bản có thời gian chạy là hằng (một
lượng thời gian nhỏ không phụ thuộc vào kích thước đầu vào n)
Trang 5Đếm số thao tác cơ bản
• Nhận diện các thao tác cơ bản trong thuật toán
• Xác định thao tác cơ bản T chiếm nhiều thời gian chạy nhất so với các thao tác cơ bản còn lại
− Thao tác T này thường xuất hiện trong các vòng lặp
• Đếm số lần thực hiện thao tác T, sẽ thu được hàm thời gian chạy f(n)
Trang 6bool isSorted(T *a, int n) {
bool sorted = true;
for (int i=0; i<n-1; i++)
if (a[i] > a[i+1]) sorted = false;
return sorted;
}
Số lần in ra màn hình = n
Số phép so sánh = n – 1
Có thể cải tiến thuật toán bên trên?
Ví dụ 2: Nhân ma trận tam giác
dưới với véctơ (mã giả)
Trang 72 Các ký hiệu tiệm cận
Trang 8• Có 3 cách phân tích tiệm cận tương ứng với ba ký
hiệu tiệm cận sau đây:
− Ô-mê-ga lớn: tìm cận dưới của f(n)
− Tê-ta lớn: tìm cận chặt của f(n)
Trang 9f(n) bị chặn trên bởi g(n)theo nghĩa tiệm cận
Trang 10f(n) bị chặn dưới bởi g(n)theo nghĩa tiệm cận
Trang 14Một số quy tắc
• Nếu f(n) = a0 + a1n + … + aknk (ak > 0)
f(n) = O(nk)
• logkn = O(n) với k là một hằng số
(hàm lôgarit tăng chậm hơn hàm tuyến tính)
Chú ý: Trong môn học này, khi viết hàm lôgarit mà không chỉ rõ cơ số, ta ngầm hiểu cơ số đó là 2
Trang 153 Tốc độ tăng của các hàm
Trang 16Tốc độ tăng của một số hàm cơ bản
Hàm Tên
log n Lôgarit log2 n Lôgarit bình phương
Trang 17Hàm nào tăng chậm hơn?
• f(n) = n log n và g(n) = n1,5
• Lời giải:
− Chú ý rằng g(n) = n1,5 = n * n0,5
− Vì vậy, chỉ cần so sánh log n và n0,5
− Tương đương với so sánh log2 n và n
− Tham khảo bảng trong slide trước: log2n tăng chậm hơn n
− Suy ra f(n) tăng chậm hơn g(n)
Trang 194 Các ví dụ phân tích thuật toán
Trang 20• Cả 4 thao tác đó được lặp lại n lần
• Thời gian chạy: t(n) = 4n = O(n)
Chú ý: Ở đây, ta bỏ qua 3 thao tác cơ bản điều khiển quá trình lặp ở dòng 1 Kết quả phân tích thuật toán sẽ không thay đổi nếu tính thêm cả 3 thao tác đó.
Trang 22• Chỉ cần cộng thời gian chạy của các vòng lặp
• Thời gian chạy tổng thể: t(n) = 3n + 5n = 8n = O(n)
Trang 23• Phân tích các vòng lặp từ trong ra ngoài:
− Vòng lặp bên trong thực hiện 3n thao tác cơ bản
− Mỗi bước lặp của vòng lặp bên ngoài thực hiện 2 + 3n thao tác cơ bản
• Thời gian chạy tổng thể: t(n) = (2 + 3n)n = 3n2 + 2n = O(n2)
Trang 24− Phép gán ở dòng 2 được thực hiện 0 hoặc 1 lần
− Phép gán ở dòng 5 được thực hiện 0 hoặc n lần
• Trong trường hợp tồi nhất (điều kiện x > 0 sai), phép gán ở dòng 5 chạy n lần
• Thời gian chạy: t(n) = 1 + n = O(n)
Trang 26= 3k + t(n – k)
• Chọn k = n – 1, khi đó:
t(n) = 3(n – 1) + t(1) = 3n – 2 = O(n)
Trang 28Tìm kiếm nhị phân
• Cho mảng a đã sắp xếp tăng dần
• Tìm x trong mảng a:
− So sánh x với phần tử ở chính giữa mảng a[mid]
− Nếu x < a[mid], tìm x ở nửa bên trái của mảng
− Nếu x > a[mid], tìm x ở nửa bên phải của mảng
− Nếu x = a[mid], báo cáo vị trí tìm được x là mid
− Nếu không còn phần tử nào để xét, báo cáo không tìm được x
Trang 30Tìm kiếm nhị phân – thuật toán
mid (left + right) / 2
else if (x > a[mid]) left mid + 1
}
return –1 }
Trang 31Tìm kiếm nhị phân – phân tích
• Nếu n = 1, chỉ mất một phép so sánh x với phần tử duy nhất của
mảng
• Nếu n > 1, mất một phép so sánh x với phần tử chính giữa mảng, sau
đó là mất thời gian tìm x trong một nửa (trái hoặc phải) của mảng
• Suy ra thời gian chạy của thuật toán:
t(n) = 1 + t(n/2) (với n > 1)
= 1 + 1 + t(n/4)
= 1 + 1 + 1 + t(n/8)
= k + t(n/2 k )
• Chọn k = log n, khi đó:
t(n) = log n + t(1) = log n + 1 = O(log n)
Trang 32Nguyễn Mạnh Hiển
hiennm@tlu.edu.vn
Trang 341 Cấu trúc dữ liệu là gì?
Trang 35Cấu trúc dữ liệu
• Là cách tổ chức dữ liệu trong máy tính sao cho các thao tác xử lý dữ liệu (như tìm, chèn, xóa) trở nên hiệu quả hơn
Trang 36Cài đặt cấu trúc dữ liệu
Mỗi cấu trúc dữ liệu được cài đặt bằng một lớp C++:
template <typename T>
class Tên-Cấu-Trúc-Dữ-Liệu {
public:
hàm tạo (constructor) hàm hủy (destructor) các thao tác xử lý private:
các trường dữ liệu các thao tác trợ giúp };
(T là kiểu dữ liệu của các phần tử trong cấu trúc dữ liệu)
Trang 372 Vector
Trang 38• Lưu trữ một dãy phần tử có kích thước thay đổi được (trong khi kích thước của mảng cố định sau khi khai báo)
• Các thao tác chính:
− Chèn và xóa phần tử ở cuối vector
− Chèn và xóa phần tử ở giữa vector
− Lấy kích thước vector
− Truy nhập phần tử dùng chỉ số
Trang 39các thao tác khác private:
int size; // kích thước vector (số phần tử) int capacity; // dung lượng vector (sức chứa)
T * array; // con trỏ tới mảng chứa các phần tử các thao tác trợ giúp
};
3 8 array
size 2 capacity 4
Trang 40Hàm tạo và hàm hủy
// initCapacity là dung lượng ban đầu của// vector, có giá trị ngầm định bằng 16Vector(int initCapacity = 16) {
Trang 41Toán tử gán
// rhs (right-hand side) là vector ở vế phải của phép gán // this là con trỏ tới vector hiện hành, tức là vế trái Vector & operator=(Vector & rhs) {
if (this != &rhs) { // ngăn cản tự sao chép
delete[] array; // xóa mảng hiện tại size = rhs.size; // đặt kích thước mới capacity = rhs.capacity; // đặt dung lượng mới array = new T[capacity]; // tạo mảng mới
// Sao chép các phần tử từ vế phải sang vế trái for (int i = 0; i < size; i++)
=
Trang 42Kích thước vector và truy nhập phần tử
// Trả về kích thước vector
int getSize() {
return size;
}
// Nếu vector rỗng, trả về true;
// ngược lại trả về false
bool isEmpty() {
return (size == 0);
}
// index là chỉ số của phần tử cần truy nhập
T & operator[](int index) {
return array[index];
}
Trang 433 Chèn phần tử
Trang 44Tăng dung lượng vector
// Đây là thao tác trợ giúp cho các thao tác chèn.
// newCapacity là dung lượng mới (phải lớn hơn kích thước) void expand(int newCapacity) {
if (newCapacity <= size)
return;
T * old = array; // old trỏ tới mảng cũ
array = new T[newCapacity]; // array trỏ tới mảng mới for (int i = 0; i < size; i++)
array[i] = old[i]; // sao chép cũ sang mới delete[] old; // xóa mảng cũ
capacity = newCapacity; // đặt dung lượng mới
}
Trang 45Chèn phần tử vào cuối vector
// newElement là phần tử mới cần chèn vào cuối vector
void pushBack(T newElement) {
// Gấp đôi dung lượng nếu vector đã đầy
Trang 46Chèn phần tử vào giữa vector
// pos (position) là vị trí chèn.
// newElement là phần tử mới cần chèn.
void insert(int pos, T newElement) {
// Gấp đôi dung lượng nếu vector đã đầy
if (size == capacity)
expand(2 * size);
// Dịch các phần tử ở pos và sau pos sang phải 1 vị trí
for (int i = size; i > pos; i )
size
pos = 1
phải dịch 8, 9,
2, 5 sang phải
Trang 474 Xóa phần tử
Trang 49Xóa phần tử ở giữa vector
// pos (position) là vị trí của phần tử cần xóa
void erase(int pos) {
// Dịch các phần tử sau pos sang trái 1 vị trí
for (int i = pos; i < size - 1; i++)
size
pos = 1
phải dịch 9, 2, 5 sang trái
Trang 505 Thời gian chạy
Trang 51Phân tích thời gian chạy
• Hàm tạo, hàm hủy: O(1)
• Toán tử gán: O(n) – vì phải sao chép các phần tử
• getSize, isEmpty, operator[]: O(1)
• expand: O(n) – vì phải sao chép các phần tử
Trang 52Danh sách liên kết (Linked Lists)
Nguyễn Mạnh Hiển
hiennm@tlu.edu.vn
Trang 53Nội dung
1 Danh sách liên kết
2 Danh sách liên kết đơn
3 Danh sách liên kết đôi
4 Danh sách liên kết vòng tròn
Trang 541 Danh sách liên kết
Trang 55− một hoặc nhiều liên kết tới các nút lân cận
• Các nút nằm rải rác trong bộ nhớ máy tính (trong khi các phần tử của mảng và vector nằm liên tục)
Trang 56Các kiểu danh sách liên kết
Danh sách liên kết đơn
Danh sách liên kết đôi
Danh sách liên kết vòng tròn
Trang 572 Danh sách liên kết đơn
Trang 58Danh sách liên kết đơn
• Có một liên kết duy nhất giữa hai nút liên tiếp
• Các thao tác chính:
− Chèn phần tử mới vào đầu danh sách
− Xóa phần tử đầu danh sách
− Lấy phần tử đầu danh sách
Trang 59Cài đặt danh sách liên kết đơn
template <typename T>
class SingleList {
public:
hàm tạo, hàm hủy chèn/xóa ở đầu danh sách lấy phần tử đầu danh sách
private:
struct Node { }; // kiểu dữ liệu của các nút Node * head; // con trỏ tới nút đầu danh sách };
Trang 60Kiểu dữ liệu của các nút
Trang 61Hàm tạo và hàm hủy
SingleList() {
head = NULL;
}
// Hàm empty kiểm tra trạng thái rỗng
// Hàm popFront xóa phần tử đầu danh sách// (tham khảo các slide sau cho hai hàm đó)
~SingleList() {
while (!empty())
popFront();
}
Trang 63Chèn vào đầu danh sách
Trang 64Chèn vào đầu danh sách (tiếp)
// e (element) là phần tử cần chèn
void pushFront(T e) {
// v là nút mới, trong đó v.next = head có// nghĩa là v trỏ tới nút đầu danh sách.Node * v = new Node(e, head);
// Nút đầu danh sách bây giờ là v, vì vậy// phải cập nhật con trỏ head
head = v;
}
Trang 65Xóa phần tử đầu danh sách
Trang 66Xóa phần tử đầu danh sách (tiếp)
void popFront() {
// Giữ lại nút đầu danh sách
Node * old = head;
// Nhảy sang nút kế tiếp
head = head->next;
// Xóa nút đầu danh sách cũ
delete old;
}
Trang 67Phân tích thời gian chạy
• Hàm tạo: O(1)
• Hàm hủy: O(n) – vì phải xóa n phần tử
• Kiểm tra rỗng: O(1)
• Lấy phần tử đầu danh sách: O(1)
• Chèn/xóa ở đầu danh sách: O(1)
Vì sao không nên chèn/xóa ở cuối danh sách liên kết đơn?
Trang 683 Danh sách liên kết đôi
Trang 69Danh sách liên kết đôi
• Mỗi nút chứa hai liên kết:
− Liên kết tới nút tiếp theo
− Liên kết về nút phía trước
• Các thao tác chính:
− Chèn/xóa ở đầu, cuối hoặc vị trí hiện hành
− Lấy phần tử ở đầu, cuối hoặc vị trí hiện hành
− Duyệt danh sách tiến hoặc lùi
• Chú ý: header và trailer là những nút giả (không chứa phần tử), được dùng để thuận tiện cho việc lập trình
Trang 70Cài đặt danh sách liên kết đôi
template <typename T> // T là kiểu phần tử class DoubleList {
public:
hàm tạo, hàm hủy, kiểm tra rỗng các thao tác chèn/xóa
các thao tác lấy phần tử các thao tác duyệt danh sách private:
struct DNode { }; // kiểu của các nút DNode * header; // đầu danh sách DNode * trailer; // cuối danh sách DNode * currentPos; // vị trí hiện hành };
Trang 71Kiểu dữ liệu của các nút
struct DNode {
T elem; // phần tử
DNode * next; // liên kết về phía sauDNode * prev; // liên kết về phía trước};
Trang 72Khai báo các thao tác
DoubleList(); // hàm tạo
~DoubleList(); // hàm hủy
bool empty(); // kiểm tra rỗng
T front(); // lấy phần tử đầu danh sách
T back(); // lấy phần tử cuối danh sách
T current(); // lấy phần tử hiện hành
bool moveNext(); // chuyển sang nút tiếp theo bool movePrevious(); // chuyển về nút phía trước void moveFront(); // chuyển về đầu danh sách
void moveBack(); // chuyển về cuối danh sách
Trang 73Khai báo các thao tác (tiếp)
void pushFront(T e); // chèn vào đầu danh sáchvoid pushBack(T e); // chèn vào cuối danh sáchvoid popFront(); // xóa ở đầu danh sách
void popBack(); // xóa ở cuối danh sách
// Chèn vào trước vị trí hiện hành
void insert(T e);
// Xóa ở vị trí hiện hành
void remove();
Trang 74Chèn vào trước vị trí hiện hành
Chèn vào trước nút này (nút v)
Đây là nút cần chèn (nút u)
Trang 75Chèn vào trước vị trí hiện hành (tiếp)
// Chèn phần tử e vào trước vị trí hiện hành
void insert(T e) {
DNode * v = currentPos; // Nút hiện hành v
DNode * u = new DNode; // Nút mới u
u->elem = e; // Nút mới u chứa e,
u->next = v; // liên kết với nút sau và u->prev = v->prev; // liên kết với nút trước v->prev->next = u; // Nút trước liên kết với u v->prev = u; // Nút sau liên kết với u
}
Trang 76Xóa ở vị trí hiện hành
Nút cần xóa (nút v)
Nút sau (nút w)Nút trước
(nút u)
Trang 77Xóa ở vị trí hiện hành (tiếp)
// Xóa nút v nằm ở vị trí hiện hành
void remove() {
DNode * v = currentPos; // Nút hiện hành v
DNode * u = v->prev; // Nút trước nút v
DNode * w = v->next; // Nút sau nút v
u->next = w; // Nút trước trỏ tới nút sau w->prev = u; // Nút sau trỏ về nút trước delete v; // Xóa nút hiện hành cũ
currentPos = w; // Vị trí hiện hành mới
}
Trang 784 Danh sách liên kết vòng tròn
Trang 79Danh sách liên kết vòng tròn
• Cấu trúc tương tự như danh sách liên kết đơn
• Nhưng có thêm con trỏ đặc biệt cursor trỏ đến cuối danh sách (back),
và liên kết next của nút cuối trỏ vòng về đầu danh sách (front)
• Các thao tác chính:
− Chèn và xóa ở sau cursor
− Lấy phần tử ở đầu và cuối danh sách
− Dịch chuyển cursor sang vị trí tiếp theo
Trang 80Cài đặt danh sách liên kết vòng tròn
template <typename T> // T là kiểu phần tử
struct CNode { }; // kiểu của các nút CNode * cursor; // con trỏ đặc biệt };
Trang 81Kiểu dữ liệu của các nút
struct CNode {
T elem; // phần tử
CNode * next; // liên kết về phía sau};
Trang 82Khai báo các thao tác
CircleList(); // hàm tạo
~CircleList(); // hàm hủy
bool empty(); // kiểm tra rỗng
T front(); // lấy phần tử đầu danh sách
T back(); // lấy phần tử cuối danh sáchvoid moveNext(); // dịch chuyển cursor
void insert(T e); // chèn vào sau cursor
void remove(); // xóa nút sau cursor
Trang 83Chèn vào sau cursor
// Chèn phần tử e vào sau cursor
void insert(T e) {
CNode * v = new CNode; // tạo nút mới v
v->elem = e; // nút mới chứa e
if (cursor == NULL) { // nếu danh sách rỗng
v->next = v; // nút v trỏ tới chính nó cursor = v; // cursor trỏ tới nút v }
else { // nếu danh sách không rỗng
v->next = cursor->next;
cursor->next = v;
}
}
Trang 84Xóa nút sau cursor
void remove() {
CNode * old = cursor->next; // nút cần xóa
if (old == cursor) // nếu danh sách chỉ có một nút
cursor = NULL; // cursor thành NULL sau khi xóa else // nếu danh sách có nhiều nút
cursor->next = old->next;
delete old;
}
Trang 85Ngăn xếp và Hàng đợi (Stacks and Queues)
Nguyễn Mạnh Hiển
hiennm@tlu.edu.vn
Trang 86Nội dung
1 Ngăn xếp
2 Hàng đợi
Trang 871 Ngăn xếp
Trang 88Ngăn xếp
• Một danh sách theo kiểu vào sau ra trước
LIFO (Last In First Out)
• Ba thao tác cơ bản (xảy ra ở đỉnh ngăn xếp):
Trang 89Cài đặt ngăn xếp – cách 1
• Cài đặt bằng danh sách liên kết đơn:
• Các thao tác:
− push : gọi thao tác pushFront của DSLK đơn
− top : gọi thao tác front của DSLK đơn
head
Trang 90Cài đặt ngăn xếp – cách 2
• Cài đặt bằng mảng:
• push(e) : topOfStack++, theArray[topOfStack] = e
• pop :
topOfStack • top : return theArray[topOfStack]
• Chú ý: Khi ngăn xếp rỗng thì topOfStack = -1
theArray
topOfStack = 3
0 1 2 3 4 5 6 7 8
Trang 91Một số ứng dụng của ngăn xếp
• Cân bằng thẻ (tag) trong một trang HTML
• Định giá biểu thức hậu tố
Trang 100Định giá biểu thức hậu tố
• Giả sử phải định giá biểu thức trung tố sau:
4,99 ∗ 1,06 + 5,99 + 6,99 ∗ 1,06
− Máy tính khoa học sẽ cho kết quả 18,69 đúng
− Máy tính giản đơn (tính tuần tự từ trái sang phải) sẽ cho kết quả 19,37 sai !
• Nếu tổ chức biểu thức dưới dạng hậu tố (toán tử viết sau các toán hạng của nó) rồi ứng dụng ngăn xếp, tính tuần tự từ trái sang phải sẽ cho kết quả đúng (tức là không cần quan tâm độ
ưu tiên của các toán tử)
4,99 1,06 ∗ 5,99 + 6,99 1,06 ∗ +
Trang 102Ví dụ
• Định giá biểu thức 6 5 2 3 + 8 ∗ + 3 + ∗
• Đặt bốn toán hạng đầu tiên vào ngăn xếp
Trang 1036 5 2 3 + 8 ∗ + 3 + ∗
• Đọc “+”, lấy 3 và 2 ra, cộng lại được 5, đặt 5 vào ngăn xếp
Trang 1046 5 2 3 + 8 ∗ + 3 + ∗
• Đặt 8 vào ngăn xếp
Trang 1056 5 2 3 + 8 ∗ + 3 + ∗
• Đọc “∗”, lấy 8 và 5 ra, nhân vào được 40, đặt
40 vào ngăn xếp
Trang 1066 5 2 3 + 8 ∗ + 3 + ∗
• Đọc “+”, lấy 40 và 5 ra, cộng lại được 45, đặt 45 vào ngăn xếp
Trang 1076 5 2 3 + 8 ∗ + 3 + ∗
• Đặt 3 vào ngăn xếp
Trang 1086 5 2 3 + 8 ∗ + 3 + ∗
• Đọc “+”, lấy 3 và 45 ra, cộng lại được 48, đặt 48 vào ngăn xếp