Chương 1 1 LỜI NÓI ĐẦU Trước khi viết một chương trình máy tính, dù đơn giản nhất, bất cứ ai, dù ở trình độ nào cũng đều phải tư duy ít nhiều về thuật toán và cách thức tổ chức lưu trữ, quản lý các dữ liệu của chương trình Trong tổng thể kiến thức về tin học, các Giải thuật (Algorithms) cùng với các Cấu trúc dữ liệu (CTDL) và thuật toán là môn học đóng vai trò quan trong quá trình đào tạo kỹ sư các ngành công nghệ thông tin và Điện tử Viễn thông Đáp ứng nhu cầu của các bạn sinh viên nói chung và.
Trang 1LỜI NÓI ĐẦU
Trước khi viết một chương trình máy tính, dù đơn giản nhất, bất cứ ai, dù ở trình
độ nào cũng đều phải tư duy ít nhiều về thuật toán và cách thức tổ chức lưu trữ, quản
lý các dữ liệu của chương trình Trong tổng thể kiến thức về tin học, các Giải thuật (Algorithms) cùng với các
Cấu trúc dữ liệu (CTDL) và thuật toán là môn học đóng vai trò quan trong quá trình đào tạo kỹ sư các ngành công nghệ thông tin và Điện tử Viễn thông Đáp ứng nhu cầu của các bạn sinh viên nói chung và sinh viên Trường Công Nghệ Thông Tin
v à Tr u yề n t h ô n g - Đạ i học Th á i ng u yê n n ó i ri ê ng , chúng tôi đã tiến hành biên soạn giáo trình Cấu trúc dữ liệu và thuật toán với mục đích nhằm giúp các bạn sinh viên chuyên ngành có một tài liệu hữu ích trong học tập, nâng cao trình độ Ngoài ra giáo trình còn là một tài liệu hữu ích cho các bạn đọc có nhu cầu tham khảo Giáo trình Cấu trúc dữ liệu và thuật toán được hình thành dựa trên cơ sở cá c sách
“Cấu trúc dữ liệu thuật toán” của tác giả Đinh Mạnh Tường, “Cấu trúc dữ liệu và giải thuật”, của tác giả Đỗ Xuân Lôi, “ Cấu trúc dữ liệu + giải thuật= Chương trình”
của N Wirth Giáo trình cũng được biên soạn dựa trên kinh nghiệm giảng dạy nhiều năm môn học trong trường
Khi lập trình giải quyết bài toán, cần đến hai dạng biểu diễn dữ liệu: biểu diễn trừu tượng và biểu diễn cụ thế Ở giai đoạn thiết kế chương trình ta cần sử dụng biểu diễn trừu tượng của dữ liệu, tới giai đoạn cài đặt chương trình ta cần sử dụng biểu diễn cụ thể của dữ liệu Biểu diễn trừu tượng của dữ liệu xác định bởi mô hình dữ liệu
(MHDL), đó là mô hình toán học của các đối tượng dữ liệu cùng với các phép toán
thực hiện trên các đối tượng dữ liệu của nó, ví dụ mô hinh dữ liệu cây, danh sách, Khi ta sử dụng mô hình dữ liệu với một số xác định các phép toán nào đó, chúng ta
sẽ có một kiểu dữ liệu trừu tượng (Abstract Data Typedef struct), ví dụ một danh sách với hai phép toán thêm vào và lấy ra chỉ được thực hiện ở một đầu của danh sách lập
thành kiểu dữ liệu trừu tượng (KDLTT) ngăn xếp (Stack)
Biểu diễn cụ thể của dữ liệu là biểu diễn xác định cách lưu trữ vật lý của dữ liệu
trong bộ nhớ của máy tính Biểu diễn cụ thể của dữ liệu được xác định bởi các CTDL Các CTDL được mô tả trong ngôn ngữ lập trình mà ta sử dụng Từ biểu diễn trừu
tượng ta có thể chuyển dịch thành các biểu diễn cụ thể khác nhau, hay nói cách khác từ
MHDL, hoặc từ KDLTT, ta có thể chuyển dịch thành các CTDL khác nhau Chẳng hạn,
ta có thể cài đặt danh sách bởi cấu CTDL mảng hoặc bởi CTDL danh sách liên kết Khi cài đặt MHDL hoặc KDLTT bởi cấu trúc dữ liệu nào đó, thì các phép toán trên mô hình được thực hiện bởi các thao tác cần thiết trên CTDL đó
Vì vậy, trong giáo trình khi nói về các CTDL ứng với từng MHDL hoặc KDLTT, phương pháp trình bày như sau: Đầu tiên chúng tôi đưa ra khái niệm về MHDL hoặc
KDLTT, sau đó đưa ra các phép toán cơ bản thường gặp trên MHDL hoặc KDLTT, và
các CTDL biểu diễn chúng Với mỗi CTDL được sử dụng để biểu diễn MHDL hoặc
KDLTT chúng tôi cài đặt các hàm và thủ tục thực hiện các phép toán cơ bản cần thiết
trên nó Từ đó cung cấp cơ sở để bạn đọc có thể thực hiện các thao tác phức thạp hơn
trên CTDL đó
Trang 2Trong giáo trình này chúng tôi sử dụng ngôn ngữ lập trình Pascal để biểu diễn
CTDL và cài đặt các phép toán, bạn đọc có thể dễ dàng chuyển sang các ngôn ngữ lập
Chương 1:
Trình bày cách tiếp cận từ một bài toán đến chương trình - mô tả quá trình từ việc mô hình hoá bài toán thực tế thành mô hình toán học đến việc lựa chọn và xây dựng cấu trúc dữ liệu, các giải thuật cho bài toán, các bước tinh chế giải thuật để đưa đến chương trình cài đặt cụ thể sử dụng một ngôn ngữ lập trình
Pascal Trong chương này chúng tôi còn cung cấp các khái niệm cơ bản về
CTDL và thuật toán, kỹ thuật đệ quy trong cách giải bài toán, các tiêu chuẩn lựa
chọn CTDL phù hợp cho bài toán và cách để phân tích đánh giá thuật toán
Chương 2:
Trình bày về MHDL danh sách, các CTDL tương ứng với mô hình này như cấu
trúc danh sách liên kết đơn, cấu trúc danh sách liên kết vòng và cấu trúc danh sách liên kết kép Ngăn xếp và hàng đợi cũng được trình bày trong chương này
và được xem là hai KDLTT từ mô hình danh sách, các CTDL tương ứng với nó
Chương này có nhiều cài đặt tương đối chi tiết để bạn đọc mới tiếp cận với lập trình có cơ hội nâng cao khả năng lập trình trong ngôn ngữ pascal, từ đó có thể chuyển dịch các cài đặt này sang các ngôn ngữ lập trình khác, ví dụ như C, Java,
Chương 3:
Chương này giới thiệu các MHDL cây bao gồm cây tổng quát, cây nhị phân, cây
nhị phân tìm kiếm Với mỗi mô hình cây chúng tôi trình bày các nội dung cần
thiết như: khái niệm, các phép toán cơ bản, các phép duyệt cây, các CTDL biểu
diễn cây, sau đó chúng tôi thực hiện việc cài đặt một số phép toán cơ bản của
mô hình trên một trong số các CTDL tương ứng với nó, bạn đọc có thể vận
dụng để cài đặt các phép toán cơ bản còn lại và cài đặt các phép toán này trên
các CTDL còn lại của mô hình Lợi thế cũng như bất lợi của từng CTDL biểu
diễn cây bạn đọc cũng nhận thức rõ qua chương này
Chương 4:
Chương này trình bày MHDL đồ thị Bạn đọc sẽ thấy rõ vai trò của đồ thị trong
việc giải quyết các bài toán ứng dụng trong thực tế Chương này chúng tôi chỉ trình bày mang tính chất giới thiệu Bạn đọc có thể tìm hiểu và tham khảo thêm
từ nhiều nguồn tài liệu khác
Chương 5:
Chương này nói về MHDL tập hợp, nội dung trình bày gồm: Định nghĩa tập
Trang 3hợp, các phép toán cơ bản trên tập hợp, các CTDL biểu diễn tập hợp như biểu
diễn tập hợp bằng vectơ bít hay bằng danh sách liên kết, danh sách kế tiếp, Chúng tôi lựa chọn dạng biểu diễn tập hợp bằng véc tơ bít để cài đặt một số phép toán cơ bản trên tập hợp, bạn đọc sẽ phải tự thảo luận và cài đặt chúng trên các dạng biểu diễn tập hợp khác Phần tiếp theo của chương trình bày kiểu dữ liệu trừu tượng từ điển (Dictionary) Phần cuối chương cho biết cấu trúc dữ liệu thích hợp để biểu diễn từ điển đó là bảng băm (Hash Table)
Để đọc và học giáo trình này, bạn đọc cần biết lập trình trên ngôn ngữ Pascal, nắm được một số thuật toán sắp xếp và tìm kiếm cơ bản như: sắp xếp nổi bọt, sắp xếp chọn, sắp xếp chèn, sắp xếp trộn, và một số thuật toán tìm kiếm như tìm kiếm tuần tự, tìm kiếm nhị phân
Mặc dù đã rất cố gắng nhiều trong quá trình biên soạn giáo trình nhưng chắc chắn giáo trình sẽ còn nhiều thiếu sót và hạn chế Nhóm tác giả rất mong nhận được
sự đóng góp ý kiến của bạn đọc để ngày một hoàn thiện hơn và có thể trở thành một giáo trình thực sự hữu ích Mọi đóng góp xin gửi qua địa chỉ: dtquy @ictu.edu.vn
Thái nguyên, 05/2019
Tác giả
Trang 4MỤC LỤC
Chương 1 MỞ ĐẦU 7
1 1 Từ bài toán đên chương trình 7
1.2 Các khái niệm cơ bản 10
1.2.1 Mô hình dữ liệu ( Data model ) 10
1.2.2 Khái niệm trừu tượng hóa 13
1.2.3 Kiểu dữ liệu trừu tượng 13
1.2.4 Dữ liệu 13
1.2.5 Biểu diễn dữ liệu trên máy tính 13
1.2.6 Kiểu dữ liệu 14
1.2.7 Cấu trúc dữ liệu (Data Structures) 16
1.2.8 Giải thuật 17
1.2.6 Mối quan hệ giữa cấu trúc dữ liệu và giải thuật 17
1.3 Phân tích giải thuật 18
1.3.1 Sự cần thiết phải phân tích giải thuật 18
1.3.2 Thời gian thực hiện của giải thuật 19
1.3.3 Không gian của giải thuật 22
1.4 Ngôn ngữ diễn đạt giải thuật 22
1.5 Đệ quy và giải thuật đệ 23
1.5.1 Một số thuật ngữ 23
1.5.2 Giải thuật đệ quy và thủ tục đệ quy 23
Chương 2 MÔ HÌNH DỮ LIỆU DANH SÁCH 25
2.1 Danh sách (List) 25
2.1.1 Khái niệm danh sách 25
2.1 2 Các phép toán cơ bản trên danh sách 25
2.1.3.1 Danh sách cài đặt bằng mảng 27
2.1.3.2 Danh sách cài đặt bởi con trỏ 29
2.2.1 Định nghĩa ngăn xếp 35
2.2.2 Các phép toán cơ bản trên ngăn xếp: 36
2.2.3 Biểu diễn ngăn xếp trên máy tính 36
2.2.4 Ứng dụng ngăn xếp 37
2.3 Hàng đợi (QUEUE) 38
Trang 52.3.1 Định nghĩa hàng đợi 38
2.3.2 Các phép toán cơ bản trên hàng 38
2.3.3 Cài đặt hàng đợi 38
2.3.3.1 Cài đặt hàng đợi bằng mảng 39
2.3.3.2 Cài đặt hàng bằng con trỏ 41
2.3.4 Một số ứng dụng của cấu trúc hàng 41
Chương 3 MÔ HÌNH DỮ LIỆU CÂY 43
3.1 Cây tổng quát 43
3.1.2 Các phép toán cơ bản trên cây 43
3.1.3 Các cách thăm (duyệt) cây 44
3.1.4 Biểu diễn cây trên máy tính 45
3.1.4.1 Biểu diễn cây bằng danh sách các con của mỗi đỉnh 45
3.1.4.2 Biểu diễn cây bằng con trưởng và em liền kề của mỗi đỉnh 48
3.1.4.3 Biểu diễn cây bởi cha của mỗi đỉnh 50
3.2 Cây nhị phân (binary tree) 50
3.2.1 Định nghĩa 50
3.2.2 Duyệt cây nhị phân 51
3.2.3 Biểu diễn cây nhị phân trên máy tính 52
3.2.4 Các phép toán cơ bản trên cây nhị phân 52
3.3 Cây tìm kiếm nhị phân (binary search tree) 54
3.3.1 Định nghĩa cây TKNP 54
3.3.2 Biểu diễn cây tìm kiếm nhị phân trên máy tính 55
3.3.3 Các phép toán cơ bản trên cây tìm kiếm nhị phân 55
Chương 4 MÔ HÌNH DỮ LIỆU ĐỒ THỊ 61
4.1 Định nghĩa đồ thị và các khái niệm 61
4.2 Các phép toán cơ bản trên đồ thị 61
4.3 Biểu diễn đồ thị 62
4.3.1 Biểu diễn đồ thị bằng ma trận kề 62
4.3.2 Biểu diễn đồ thị bằng danh sách các đỉnh kề 63
4.4 Các phép duyệt đồ thị (TRAVERSALS OF GRAPH) 63
4.4.1 Duyệt theo chiều sâu (depth-first search) 63
4.4.2 Duyệt theo chiều rộng (breadth-first search) 63
4.5 Một số bài toán ứng dụng trên đồ thị: 63
Trang 6Chương 5 MÔ HÌNH DỮ LIỆU TẬP HỢP 66
5.1 Khái niệm tập hợp 66
5 2 Mô hình dữ liệu tập hợp 66
5.3 Biểu diễn tập hợp trên máy tính và cài đặt các phép toán trên tập hợp 67
5.3.1.Cài đặt tập hợp bởi vectơ bit 67
5.3.2 Cài đặt tập hợp bởi mảng 68
5.3.3 Cài đặt bởi danh sách liên kết hoặc danh sách được sắp 68
5.4 Từ điển (Dictionary) 69
5.4.1 Từ điển là gì? 69
5.4.2 Các phương pháp cài đặt từ điển 69
5.4.3 Cấu trúc dữ liệu Bảng băm, cài đặt từ điển bởi bảng băm 70
5.4.3.1 Cài đặt từ điển bằng bảng băm mở 70
5.4.3.2 Cài đặt từ điển bằng bảng băm đóng 72
5.4.3.3 Các phương pháp xác định hàm băm 73
Trang 7Chương 1 MỞ ĐẦU
1 1 Từ bài toán đên chương trình
Để lập trình giải một bài toán bằng máy tính ta thường trải qua 2 giai đoạn giai đoạn
1: thiết kế - nhằm xác định bài toán cần giải quyết và xây dựng mô hình toán học cho
bài toán Mục đích của giai đoạn này là nhằm trả lời hai câu hỏi: Bài toán cho cái gì
và yêu cầu làm những gì? sau đó trả lời câu hỏi “để thực hiện các yêu cầu của bài
toán thì làm như thế nào” Giai đoạn 2: Mã hóa - Sử dụng ngôn ngữ lập trình cụ thể
để viết chương trình ứng với cách làm của giai đoạn trước nó
1.1.1 Xác định & mô hình hóa bài toán cần giải quyết
Khi giải quyết 1 bài toán thực tế ta phải bắt đầu từ việc xác định bài toán, giai đoạn này cần nhiều thời gian và công sức bỏ ra để thực hiện Nó nhằm trả lời rõ ràng các câu hỏi: 1) bài toán yêu cầu ta "phải làm những công việc gì?"; 2) để thực hiện các công việc thì ta phải "làm như thế nào?" Thông thường, khi khởi đầu, hầu hết các bài toán là không đơn giản, không rõ ràng Để giảm bớt sự phức tạp của bài toán thực tế, ta phải hình thức hóa nó, nghĩa là phát biểu lại bài toán thực tế thành một bài toán hình thức (hay còn gọi là mô hình toán) Có thể có rất nhiều bài toán thực
tế có cùng một mô hình toán
Ví dụ 1: Xét bài toán tô màu bản đồ thế giới
Ta cần phải tô màu cho các nước trên bản đồ thế giới Trong đó mỗi nước đều được tô một màu và hai nước láng giềng (cùng biên giới) thì phải được tô bằng hai màu khác nhau Hãy tìm một phương án tô màu sao cho số màu sử dụng là ít nhất
Việc lựa chọn và xây dựng mô hình toán học cho bài toán diễn ra theo phân tích sau:
Có thể xem mỗi nước trên bản đồ thế giới là một đỉnh của đồ thị, hai nước láng giềng của nhau thì hai đỉnh ứng với nó được nối với nhau bằng một cạnh Bài toán lúc này trở thành bài toán tô màu cho đồ thị:
Mỗi đỉnh đều phải được tô màu,
Hai đỉnh có cạnh nối thì phải tô bằng hai màu khác nhau và ta cần tìm một phương án tô màu sao cho số màu được sử dụng là ít nhất
Mô hình toán học được sử dụng trong bài toán này là mô hình đồ thị
Ví dụ 2: Xét bài toán điều khiển đèn giao thông
Cho một ngã năm như hình 1.1, trong đó C và E là các đường một chiều theo chiều
mũi tên, các đường khác là hai chiều Hãy thiết kế một bảng đèn hiệu điều khiển giao thông tại ngã năm này một cách hợp lý, nghĩa là: phân chia các lối đi tại ngã năm này thành các nhóm, mỗi nhóm gồm các lối đi có thể cùng đi đồng thời nhưng không xảy ra tai nạn giao thông (các lối đi này có các hướng đi không cắt nhau), và số lượng nhóm chia là ít nhất có thể được
Trang 8Việc lựa chọn, xây dựng mô hình toán học cho bài toán được diễn ra theo phân tích sau:
Có thể xem đầu vào của bài toán là tất cả các lối đi tại ngã năm này, đầu ra của bài toán là các nhóm lối đi có thể đi đồng thời mà không xảy ra tai nạn giao thông, mỗi nhóm sẽ tương ứng với một pha điều khiển của đèn hiệu, vì vậy ta phải tìm kiếm lời giải với số nhóm là ít nhất để giao thông không bị tắc nghẽn vì phải chờ đợi quá lâu Trước hết ta nhận thấy rằng tại ngã năm này có 13 lối đi: AB, AC, AD, BA, BC,
BD, DA, DB, DC, EA, EB, EC, ED Tất nhiên, để có thể giải được bài toán ta phải tìm một cách nào đó để thể hiện mối liên quan giữa các lối đi này Lối nào với lối nào không thể đi đồng thời, lối nào và lối nào có thể đi đồng thời Ví dụ cặp AB và EC có thể đi đồng thời, nhưng AD và EB thì không, vì các hướng giao thông cắt nhau Ở đây ta sẽ dùng một sơ đồ trực quan như sau: tên của 13 lối đi được viết lên mặt phẳng, hai lối đi nào nếu đi đồng thời sẽ xảy ra đụng nhau (tức là hai hướng đi cắt qua nhau) ta nối lại bằng một đoạn thẳng, hoặc cong, hoặc ngoằn ngoèo tuỳ thích Ta
sẽ có một sơ đồ như hình 1.2 Như vậy, trên sơ đồ này, hai lối đi có cạnh nối lại với
nhau là hai lối đi không thể cho đi đồng thời
Trang 9Với cách biểu diễn như vậy ta đã có một mô hình toán học đồ thị (Graph), trong đó mỗi lối đi trở thành một đỉnh của đồ thị, hai lối đi không thể cùng đi đồng thời được nối nhau bằng một đoạn ta gọi là cạnh của đồ thị Bây giờ ta phải xác định các nhóm, với số nhóm ít nhất, mỗi nhóm gồm các lối đi có thể đi đồng thời, nó ứng với một pha của đèn hiệu điều khiển giao thông Giả sử rằng, ta dùng màu để tô lên các đỉnh của đồ thị này sao cho:
(1) Các lối đi cho phép cùng đi đồng thời sẽ có cùng một màu: Dễ dàng nhận thấy rằng hai đỉnh có cạnh nối nhau sẽ không được tô cùng màu
( 2 ) Số nhóm là ít nhất: ta phải tính toán sao cho số màu được dùng là ít nhất Tóm lại,
ta phải giải quyết bài toán sau: "Tô màu cho đồ thị ở hình 1.2 sao cho:
Hai đỉnh có cạnh nối với nhau (hai còn gọi là hai đỉnh kề nhau) không cùng màu
Số màu được dùng là ít nhất."
Trở về bài toán tương tự ví dụ trước
Cả hai bài toán trên, ban đầu có vẻ rất khác nhau, nhưng sau khi phân tích để hình thức hóa thì chúng đều được đưa về mô hình toán học đồ thị, và áp dụng thuật toán tô mầu trên đồ thị để giải quyết các bài toán này
Một bài toán thực tế bất kỳ thường bao gồm các đối tượng dữ liệu và các yêu cầu xử lý trên những đối tượng đó, cho nên trong giai đoạn phân tích và thiết kế, khi xây dựng
mô hình toán học cho bài toán cần chú trọng đến hai vấn đề :
(1) Tổ chức biểu diễn các đối tượng dữ liệu của bài toán trong mô hình toán học như thế nào? mô hình này ta còn gọi là mô hình dữ liệu trong tin học ?
(2) Xây dựng các thao tác xử lý trên các đối tượng của mô hình ra sao ?
1.1.2 Cài đặt chương trình cho bài toán cần giải quyết
Khi cài đặt chương trình giải quyết bài toán tương ứng ta quan tâm đến hai vấn đề:
1) Biểu diễn mô hình dữ liệu/mô hình toán học của bài toán trên máy tính
Ta phải chọn dạng biểu diễn nào để máy tính có thể hiểu và thực hiên các thao tác trên chúng Giai đoạn này còn được gọi là xây dựng cấu trúc dữ liệu cho bài toán Ta có thể cài đặt một mô hình dữ liệu bởi nhiều cấu trúc dữ liệu khác nhau Trong mỗi cách cài đặt, một số phép toán trên mô hình có thể được thực hiện thuận lơi, nhưng các phép toán khác có thể lại không thuận lợi
2) Mã hóa các giải thuật
Với mỗi cấu trúc dữ liệu ta cần mã hóa các thao xử lý trên nó để giải quyết các yêu cầu đặt ra của bài toán
Ta có thể sử dụng một ngôn ngữ lập trình cụ thể nào đó (Pascal,C, ) để cài đặt
kết quả ở giai đoạn 1, ở bước này ta dùng các cấu trúc dữ liệu được cung cấp
trong ngôn ngữ, ví dụ Array, Record, để biểu diễn m ô h ì n h d ữ l i ệ u c ủ a
b à i t o á n t r ê n m á y t í n h , và mã hóa giải thuật bởi các câu lệnh trong ngôn ngữ lập trình lựa chọn
Tóm lược các bước từ bài toán đến chương trình:
1) Về mặt dữ liệu
Trang 10Theo quy trình từ: Mô hình dữ liệu -> Kiểu dữ liệu trừu tượng -> Cấu trúc dữ liệu Thật vậy: Trong quá trình phát triển chương trình, nhất là khi phát triển các hệ thống phần mềm lớn, ta cần đến hai dạng biểu diễn dữ liệu: Biếu diễn trừu tượng và biểu diễn cụ thể
a) Trong giai đoạn xác định và mô hình hóa bài toán: ta cần sử dụng dạng
biểu diễn trừu tượng: được xác định bởi mô hình dữ liệu – đó là mô hình toán học, ví dụ như: Mô hình cây, danh sách, tập hợp, đồ thị, mô hình ERA, …Khi ta dùng mô hình dữ liệu với một số xác định các phép toán nào đó, ta sẽ có một kiểu dữ liệu trừu tượng , ví dụ: Ngăn xếp, hàng đợi, bảng băm, …
=> Dạng biểu diễn dữ liệu này không phụ thuộc vào ngôn ngữ lập trình
cụ thể
b) Trong giai đoạn cài đặt chương trình, ta cần sử dụng dạng biểu diễn cụ
thể của dữ liệu: Là biểu diễn xác định cách lưu trữ vật lý của dữ liệu trong
bộ nhớ máy tính Biểu diễn cụ thể của dữ liệu được xác định bởi các cấu trúc dữ liệu Các cấu trúc dữ liệu được mô tả trong ngôn ngữ lập trình cụ thể mà ta sử dụng
=> Dạng biểu diễn này phụ thuộc vào ngôn ngữ lập trình cụ thể
Từ biểu diễn trừu tượng, ta có thể chuyển dịch thành các biểu diễn cụ thể khác nhau, hay các cấu trúc dữ liệu khác nhau Ví dụ, ta có thể biểu diễn danh sách bởi mảng hoặc bằng con trỏ, tương ứng ta thu được CTDL kế tiếp, CTDL danh sách liên kết đơn, đôi, vòng…
2) Về mặt xử lý dữ liệu
Theo quy trình từ: Giải thuật phi hình thức -> giải thuật bằng giả ngôn -> Giải thuật được mã hóa hoàn toàn bởi ngôn ngữ lập trình cụ thể Thật vây: từ những yêu cầu xử lý của bài toán, ta xây dựng c á c giải thuật trên mô hình d ữ l i ệ u
đ ã x â y d ự ng Giải thuật có thể mô tả một cách phi hình thức - nó chỉ nêu phương pháp giải hoặc các bước giải một cách tổng quát Tiếp theo ta hình thức hoá giải thuật bằng ngôn ngữ giả, rồi chi tiết hoá dần ("mịn hoá") các bước giải tổng quát ở trên (làm mịn dần), ở bước này ta cần dùng các kiểu dữ liệu trừu tượng (MHDL hoặc KDLTT) và các cấu trúc lệnh điều khiển trong ngôn ngữ lập trình (không chú trọng đến cú pháp ngôn ngữ) , kết hợp ngôn ngữ tự nhiên để mô tả giải thuật Cuối cùng trong pha cài đặt, ta tiến hành mã hóa hoàn toàn giải thuật được mô tả bởi ngôn ngữ giả, sử dụng ngôn ngữ lập trình
cụ thể, thao tác trên CTDL cụ thể
1.2 Các khái niệm cơ bản
1.2.1 Mô hình dữ liệu ( Data model )
Mô hình dữ liệu là gì?
Là mô hình dữ liệu được sử dụng để mô tả cấu trúc logic của dữ liệu được xử lý bởi hệ thống Là mô hình toán học cùng với các phép toán có thể thực hiện trên các đối tượng của mô hình
Trang 11Lựa chọn mô hình dữ liệu cho bài toán cần giải quyết như thế nào?
Các thành phần dữ liệu thực tế đa dạng, phong phú và thường chứa đựng những quan
hệ nào đó với nhau, do đó cần phải tổ chức, lựa chọn và xây dựng các mô hình dữ liệu thích hợp nhất sao cho vừa có thể phản ánh chính xác các dữ liệu thực tế này, vừa có thể dễ dàng dùng máy tính để xử lý Để tìm ra cấu trúc toán học thích hợp với một bài toán đã cho, trước hết chúng ta cần phải phân tích kỹ bài toán để tìm ra câu trả lời cho các câu hỏi sau:
(1) Các thông tin quan trọng của bài toán có thể biểu diễn bởi các đối tượng toán học nào ?
(2) Có các mối quan hệ nào giữa các đối tượng ?
(3) Các kết quả phải tìm của bài toán có thể biểu diễn bởi các khái niệm toán học nào ?
Sau khi đã có mô hình toán học mô tả bài toán, một câu hỏi đặt ra là, ta phải làm việc với mô hình như thế nào để giải quyết các yêu cầu của bài toán? Chúng ta sẽ thiết kế các thuật toán thông qua các hành động, các phép toán thực hiện trên các đối tượng của mô hình
Các loại mô hình dữ liệu
Có thể phân loại các mô hình dữ liệu dựa trên mối quan hệ giữa các phần tử của chúng như sau:
1- Mô hình dữ liệu tuyến tính (danh sách): Dùng để biểu diễn các phần tử có
quan hệ 1:1 Các phần tử trong mô hình có quan hệ tuyến tính theo thứ tự xuất hiện của chúng, tức là nếu mô hình dữ liệu tuyến tính chứa các phần tử thì nó phải có phần tử đầu tiên và phần tử cuối cùng, mỗi phần tử có đúng
một phần đứng ngay trước và một phần tử đứng ngay sau Hình 1.3 biểu
diễn một ví dụ về mô hình dữ liệu tuyến tính
Hình 1.3 Mô hình dữ liệu danh sách 2- Mô hình dữ liệu phân cấp (mô hình cây): Dùng để biểu diễn các phần tử
có quan hệ 1: n, tức là mỗi phần tử trong mô hình có nhiều hậu bối - con,
nhưng chỉ có một tiền bối - cha Hình 1.4 biểu diễn một ví dụ cụ thể về mô
Phần tử cuối cùng
Phần
tử đầu
tiên
Phần tử đứng sau A
A Phần tử đứng trước A
Trang 12hình này Nếu ta di chuyển từ trên xuống dưới trong Hình 1.4 thì mỗi nút có
thể trỏ đến nhiều nút khác – các con, nhưng nếu ta di chuyển từ dưới lên thì mỗi nút (trừ nút ở gốc) chỉ có quan hệ với 1 nút - cha Mô hình dữ liệu phân cấp như vậy thường được gọi là cây, và đây là một loại mô hình dữ liệu quan trọng trong khoa học máy tính:
Hình 1.4 – Mô hình dữ liệu cây
3 - Mô hình dữ liệu đồ thị: đây là mô hình dữ liệu phong phú và phức tạp
nhất Trong đồ thị, các phần tử có mối quan hệ n:m Tức là, mỗi phần tử
có thể có quan hệ với một hoặc nhiều phần tử khác Hình 1.5 biểu diễn
một ví dụ cụ thể về mô hình này
Hình 1.5 – Mô hình dữ liệu đồ thị
4 - Mô hình dữ liệu tập hợp Trong một tập hợp, các phần tử không có mối
quan hệ trực tiếp với nhau, giữa chúng chỉ có một mối quan hệ là thành viên của tập hợp, ta không cần quan tâm tới vị trí chính xác của một
phần tử nào đó trong tập hợp Hình 1.6 biểu diễn một ví dụ cụ thể về mô
hình này: trong khuôn mặt người có nhiều đối tượng khác nhau như mắt, mũi, miệng, …
A
A chỉ có 1 tiền bối,
có nhiều hậu bối
Các hậu bối của A
Tiền bối của A
A
Trang 131.2.2 Khái niệm trừu tượng hóa
Trong tin học, trừu tượng hóa nghĩa là đơn giản hóa, làm cho sáng sủa hơn và dễ hiểu
hơn Cụ thể trừu tượng hóa là che đi những chi tiết, làm nổi bật cái tổng thể
1.2.3 Kiểu dữ liệu trừu tượng
Trong Mô hình dữ liệu, chúng ta có thể thực hiện một tập hợp các phép toán rất đa dạng, phong phú Song trong nhiều áp dụng, chúng ta chỉ sử dụng mô hình với một số xác định các phép toán nào đó Khi đó chúng ta sẽ có một kiểu dữ liệu trừu tượng
Kiểu dữ liệu trừu tượng (abstract data typedef struct - KDLTT): là một mô hình
dữ liệu được xét cùng với một số xác định các phép toán Ví dụ, mô hình dữ
liệu danh sách, chỉ xét đến các phép toán thêm vào và lấy ra, ta gọi là KDLTT hàng đợi hoặc KDLTT ngăn xếp, Mô hình tập hợp, chỉ xét đến các phép toán: Thêm vào, loại bỏ, tìm kiếm ta gọi là KDLTT từ điển,
Dữ liệu đầu vào: số nguyên n, dãy số a1, a2, , an
Dữ liệu đầu ra: Số lớn nhất (giả sử max)
Dữ liệu trung gian: Giả sử dùng biến điều khiển i trong quá trình duyệt từ đầu đến cuối dãy để tìm số lớn nhất
1.2.5 Biểu diễn dữ liệu trên máy tính
Trong MTĐT, các dữ liệu dù tồn tại ở những hình thức khác nhau (số, văn bản, hình ảnh, đúng / sai ) đều được biểu diễn dưới dạng nhị phân khi đưa vào MT xử lý Tức
là mỗi dữ liệu được biểu diễn dưới dạng một dãy các số nhị phân 0 hoặc 1 Ví dụ: Số
10 = 0000 1010 ở dạng nhị phân Cách biểu diễn này rất không thuận tiện (dài, khó, không gợi nhớ, …) đối với con người Việc xuất hiện các ngôn ngữ lập trình bậc cao (
Trang 14Pascal, C, … gần với ngôn ngữ tự nhiên) đã giải phóng con người khỏi những khó khăn khi làm việc với cách biểu diễn dữ liệu nhị phân trong MT
Trong các ngôn ngữ lập trình bậc cao: Các kiểu dữ liệu là sự trừu tượng hoá các tính chất của các đối tượng dữ liệu có cùng bản chất trong thế giới thực (chỉ ra những tính chất đặc trưng cho các đối tượng thuộc phạm vi bài toán đang xét) Ví dụ, ứng với các
dữ liệu dạng số, tương ứng ta có các kiểu dữ liệu số nguyên, số thực, số phức, … trong ngôn ngữ lập trình
Như vậy tất cả các dữ liệu mô tả trong ngôn ngữ lập trình bậc cao được máy tính xử lý đều phải được khai báo thuộc một kiểu dữ liệu xác định Khi đó chương trình dịch của ngôn ngữ lập trình bậc cao sẽ dịch chuyển các kiểu dữ liệu này sang dạng biểu diễn nhị phân trước khi được máy tính xử lý
1.2.6 Kiểu dữ liệu
Mỗi kiểu dữ liệu trong ngôn ngữ lập trình thường đại diện cho một loại dữ liệu trong thực tế Ví dụ chúng ta có kiểu dữ liệu số nguyên đại diện cho tất cả các đối tượng dữ liệu là số nguyên, tất cả các dữ liệu được xử lý trong chương trình đều phải thuộc kiểu
dữ liệu xác định được khai báo trong ngôn ngữ lập trình
Trong ngôn ngữ lập trình, một kiểu dữ liệu T được xác định bởi một bộ <V,O> trong
đó:
V : tập các giá trị hợp lệ mà một đối tượng kiểu T có thể lưu trữ
O : tập các thao tác xử lý có thể thi hành trên đối tượng kiểu T
Ví du: Giả sử có kiểu dữ liệu ký tự (Chacracter) = <Vc ,Oc> với
Tên kiểu: Từ khóa thể hiện cho kiểu đó
Miền giá trị: thể hiện phạm vi giá trị của các biến thuộc kiểu này
Kích thước: Tổng số byte mà một biến thuộc kiểu chiếm
Tập các toán tử: Các phép toán cơ bản mà kiểu dữ liệu cung cấp
Các kiểu dữ liệu được chia làm 2 nhóm, đó là nhóm gồm các kiểu dữ liệu cơ bản - thường là các loại dữ liệu đơn giản, không có cấu trúc Chúng thường là các giá trị vô hướng như các số nguyên, số thực, các ký tự, các giá trị logic Các loại dữ liệu này,
do tính thông dụng và đơn giản của mình, thường được các ngôn ngữ lập trình (NNLT) cấp cao xây dựng sẵn như một thành phần của ngôn ngữ để giảm nhẹ công việc cho người lập trình Chính vì vậy đôi khi người ta còn gọi chúng là các kiểu dữ liệu định sẵn/tiền định Thông thường, các kiểu dữ liệu cơ bản bao gồm :
Trang 15Kiểu có thứ tự rời rạc: số nguyên, ký tự, logic , liệt kê, miền con …
Kiểu không rời rạc: số thực
Các kiểu cơ sở rất đơn giản và không thể hiện rõ sự tổ chức dữ liệu trong một cấu trúc, thường chỉ được sử dụng làm nền để xây dựng các kiểu dữ liệu phức tạp khác Trong nhiều trường hợp, chỉ với các kiểu dữ liệu cơ sở không đủ để phản ánh tự nhiên và đầy
đủ bản chất của sự vật thực tế, dẫn đến nhu cầu phải xây dựng các kiểu dữ liệu mới dựa trên việc tổ chức, liên kết các thành phần dữ liệu có kiểu dữ liệu đã được định nghĩa Những kiểu dữ liệu được xây dựng như thế gọi là kiểu dữ liệu có cấu trúc, thuộc nhóm các kiểu dữ liệu có cấu trúc Đa số các ngôn ngữ lập trình đều cài đặt sẵn một số kiểu có cấu trúc cơ bản như mảng, chuỗi, tập tin, bản ghi và cung cấp cơ chế cho lập trình viên tự định nghĩa kiểu dữ liệu mới Ví dụ để mô tả một đối tượng sinh viên, cần quan tâm đến các thông tin sau:
- Mã sinh viên: chuỗi ký tự
- Tên sinh viên: chuỗi ký tự
- Ngày sinh: kiểu ngày tháng
- Nơi sinh: chuỗi ký tự
- Điểm thi: số nguyên Các kiểu dữ liệu cơ sở cho phép mô tả một số thông tin như Diemthi: integer;
Các thông tin khác đòi hỏi phải sử dụng các kiểu có cấu trúc như:
Trang 16} SinhVien;
Giả sử đã có cấu trúc phù hợp để lưu trữ một sinh viên, nhưng thực tế lại cần quản lý nhiều sinh viên, lúc đó nảy sinh nhu cầu xây dựng kiểu dữ liệu có cấu trúc mới, ví dụ danh sách hoặc mảng
Mục tiêu của việc nghiên cứu cấu trúc dữ liệu chính là tìm những phương cách thích hợp để tổ chức, liên kết dữ liệu, hình thành các kiểu dữ liệu có cấu trúc từ những kiểu
dữ liệu đã được định nghĩa
1.2.7 Cấu trúc dữ liệu (Data Structures)
Cấu trúc dữ liệu được mô tả như sau: CTDL = { Các dữ liệu thành phần} (1)
Trong đó, các dữ liệu thành phần có thể là dữ liệu đơn (sẵn có) hoặc là CTDL đã được xây dựng, chúng được liên kết với nhau theo một phương pháp liên kết nào đó Ví dụ
một số CTDL như mảng: Bao gồm một dãy có thứ tự các phần tử có cùng 1 kiểu dữ liệu xác định nào đó, Bản ghi: Bao gồm một tập các phần tử có thể thuộc các kiểu dữ
liệu khác nhau, có mối quan hệ với nhau đó là chúng cùng mô tả các thuộc tính của một đối tượng, ví dụ thông tin về 1 con người gồm họ tên, ngày sinh, chiều cao, cân nặng, (1) là cách mô tả cấu trúc dữ liệu trực tiếp nhất, ngoài ra người ta có thể định nghĩa cấu trúc dữ liệu thông qua các khái niệm khác như CTDL là MHDL/KDLTT được biểu diễn trên máy tính, hay cấu trúc dữ liệu là kiểu dữ liệu có cấu trúc
Các tiêu chuẩn lựa chọn cấu trúc dữ liệu liệu cho bài toán:
1 - Phản ánh đúng thực tế
Đây là tiêu chuẩn quan trọng nhất, quyết định tính đúng đắn của toàn bộ bài toán Cần xem xét kỹ lưỡng cũng như dự trù các trạng thái biến đổi của dữ liệu trong chu trình sống để có thể chọn cấu trúc dữ liệu lưu trữ thể hiện chính xác đối tượng thực tế Một
số tình huống chọn cấu trúc lưu trữ sai :
Chọn một biến số nguyên integer để lưu trữ tiền thưởng bán hàng (được tính theo
công thức tiền thưởng bán hàng = trị giá hàng * 5%), do vậy sẽ làm tròn mọi giá trị tiền thưởng gây thiệt hại cho nhân viên bán hàng Trường hợp này phải sử dụng biến
số thực để phản ánh đúng kết quả của công thức tính thực tế
Trong trường trung học, mỗi lớp có thể nhận tối đa 28 học sinh Lớp hiện có 20 học sinh, mỗi tháng mỗi học sinh đóng học phí 100.000 đ Chọn một biến số nguyên
byte ( khả năng lưu trữ 0 - 255) để lưu trữ tổng học phí của lớp học trong tháng là
không phù hợp vì giá trị tổng học phí thu được > 255, vượt khỏi khả năng lưu trữ của biến đã chọn, gây ra tình trạng tràn, dẫn đến sai lệch
2 - Phù hợp với các thao tác trên đó
Tiêu chuẩn này giúp tăng tính hiệu quả của giải thuật, cụ thể là giúp cho việc phát triển các thuật toán đơn giản, tự nhiên hơn trên cấu trúc Do đó chương trình đạt hiệu quả cao hơn về tốc độ xử lý Một tình huống chọn cấu trúc lưu trữ không phù hợp như khi cần xây dựng một chương trình soạn thảo văn bản, các thao tác xử lý thường xảy ra là chèn, xoá sửa các ký tự trên văn bản Trong thời gian xử lý văn bản, nếu chọn cấu trúc lưu trữ văn bản trực tiếp lên tập tin thì sẽ gây khó khăn khi xây dựng các giải thuật cập nhật văn bản và làm chậm tốc độ xử lý của chương trình vì phải làm việc trên bộ nhớ
Trang 17ngoài Trường hợp này nên tìm một cấu trúc dữ liệu có thể tổ chức ở bộ nhớ trong để lưu trữ văn bản suốt thời gian soạn thảo
LƯU Ý :
Đối với mỗi ứng dụng, cần chú ý đến thao tác nào được sử dụng nhiều nhất để lựa chọn cấu trúc dữ liệu cho thích hợp
3 - Tiết kiệm tài nguyên hệ thống
Cấu trúc dữ liệu chỉ nên sử dụng tài nguyên hệ thống vừa đủ để đảm nhiệm được chức năng của nó Thông thường có 2 loại tài nguyên cần lưu tâm nhất: CPU và bộ nhớ Tiêu chuẩn này nên cân nhắc tùy vào tình huống cụ thể khi viết chương trình Nếu cần một chương trình có tốc độ xử lý nhanh thì khi chọn cấu trúc dữ liệu, yếu tố tiết kiệm thời gian xử lý phải đặt nặng hơn tiêu chuẩn sử dụng tối ưu bộ nhớ, và ngược lại Một
số tình huống chọn cấu trúc lưu trữ lãng phí:
Sử dụng biến integer (2 bytes) để lưu trữ một giá trị cho biết tháng hiện hành Biết rằng tháng chỉ có thể nhận các giá trị từ 1-12, nên chỉ cần sử dụng kiểu byte là đủ
Để lưu trữ danh sách học viên trong một lớp, sử dụng mảng 50 phần tử (giới hạn số học viên trong lớp tối đa là 50) Nếu số lượng học viên thật sự ít hơn 30, thì gây lãng phí Trường hợp này cần có một cấu trúc dữ liệu linh động hơn mảng, ví dụ danh sách liên kết – ta sẽ đề cập đến trong các chương tiếp theo
Tóm lại
Kiểu dữ liệu phức/có cấu trúc, hay còn gọi là cấu trúc dữ liệu, là kiểu dữ liệu trong đó các phần tử của nó có thể phân tách thành các kiểu dữ liệu đơn hoặc kiểu dữ liệu phức khác
1.2.8 Giải thuật
Khi đã có mô hình toán học thích hợp cho một bài toán ta cần cố gắng tìm cách giải quyết bài toán trong mô hình đó Khởi đầu là tìm một giải thuật Knuth (1973)
định nghĩa: “giải thuật là một chuỗi hữu hạn các thao tác để giải một bài toán nào
đó” Các tính chất quan trọng của giải thuật là:
- Hữu hạn (finiteness): Giải thuật phải luôn luôn kết thúc sau một số hữu hạn
bước
- Xác định (definiteness): Mỗi bước của giải thuật phải được xác định rõ ràng
và phải được thực hiện chính xác, nhất quán
- Hiệu quả (effectiveness): các thao tác trong giải thuật phải được thực hiện
trong một lượng thời gian hữu hạn
Ngoài ra một giải thuật còn phải có đầu vào (input) và đầu ra (output).M ột giải thuật phải giải quyết xong công việc khi ta cho dữ liệu vào Có nhiều cách để thể hiện giải thuật: dùng ngôn ngữ tự nhiên, dùng lưu đồ, dùng ngôn ngữ giả, dùng các câu lệnh của ngôn ngữ lập trình, Và một cách dùng phổ biến là dùng ngôn ngữ giả,
đó là sự kết hợp của ngôn ngữ tự nhiên và các câu lệnh của ngôn ngữ lập trình
1.2.6 Mối quan hệ giữa cấu trúc dữ liệu và giải thuật
Trong một chương trình: Giải thuật phản ánh các phép xử lý, còn đối tượng xử lý của giải thuật lại là dữ liệu, chính dữ liệu chứa đựng các thông tin cần thiết để thực hiện
Trang 18giải thuật Để xác định được giải thuật phù hợp cần phải biết nó tác động đến loại dữ liệu nào (ví dụ để làm nhuyễn các hạt đậu, người ta dùng cách xay chứ không băm bằng dao, vì đậu sẽ văng ra ngoài) và khi chọn lựa cấu trúc dữ liệu cũng cần phải hiểu
rõ những thao tác nào sẽ tác động đến nó (ví dụ để biểu diễn các điểm số của sinh viên người ta dùng số thực thay vì chuỗi ký tự vì còn phải thực hiện thao tác tính trung bình
từ những điểm số đó)
Trong một chương trình máy tính, giải thuật và cấu trúc dữ liệu có mối quan hệ chặt
chẽ với nhau, được thể hiện qua công thức Cấu trúc dữ liệu + Giải thuật = Chương trình Với một cấu trúc dữ liệu đã chọn, sẽ có những giải thuật tương ứng, phù hợp
Khi cấu trúc dữ liệu thay đổi thường giải thuật cũng phải thay đổi theo để tránh việc
xử lý gượng ép, thiếu tự nhiên trên một cấu trúc không phù hợp Hơn nữa, một cấu trúc dữ liệu tốt sẽ giúp giải thuật xử lý trên đó có thể phát huy tác dụng tốt hơn, vừa đáp ứng nhanh vừa tiết kiệm bộ nhớ, giải thuật cũng dễ hiễu và đơn giản hơn
* Cấu trúc lưu trữ ( Storange structures)
CTDL được biểu diễn trong bộ nhớ máy tính còn được gọi là Cấu trúc lưu trữ CTDL
được lưu trữ trong bộ nhớ trong được gọi là cấu trúc lưu trữ trong, ví dụ: mảng, bản
ghi, CTDL được lưu trữ trong bộ nhớ ngoài được gọi là cấu trúc lưu trữ ngoài, ví dụ
tệp tin, bảng Trong giáo trình này chúng tôi chủ yếu tập trung trên các cấu trúc lưu trữ trong
1.3 Phân tích giải thuật
1.3.1 Sự cần thiết phải phân tích giải thuật
Trong khi giải một bài toán chúng ta có thể có một số giải thuật khác nhau, vấn đề là cần phải đánh giá các giải thuật đó để lựa chọn một giải thuật tốt nhất Thông thường thì ta sẽ căn cứ vào các tiêu chuẩn sau:
(1) - Giải thuật đúng đắn
(2) - Giải thuật đơn giản
(3) - Giải thuật hiệu quả
Với yêu cầu (1), để kiểm tra tính đúng đắn của giải thuật chúng ta có thể cài đặt giải thuật đó và cho thực hiện trên máy với một số bộ dữ liệu mẫu rồi lấy kết quả thu được so sánh với kết quả đã biết Thực ra thì cách làm này không chắc chắn bởi vì có thể giải thuật đúng với tất cả các bộ dữ liệu chúng ta đã thử nhưng lại sai với một bộ dữ liệu nào đó Vả lại cách làm này chỉ phát hiện ra giải thuật sai chứ chưa chứng minh được là nó đúng Tính đúng đắn của giải thuật cần phải được chứng minh bằng toán học Tất nhiên điều này không đơn giản và do vậy chúng ta sẽ không đề cập đến ở đây
Khi viết một chương trình để sử dụng một vài lần thì yêu cầu (2) là quan trọng nhất Chúng ta cần một giải thuật dễ viết chương trình để nhanh chóng có được kết quả, thời gian thực hiện chương trình không được đề cao vì dù sao thì chương trình đó cũng chỉ sử dụng một vài lần
Khi một chương trình được sử dụng nhiều lần thì thì yêu cầu tiết kiệm thời gian thực hiện chương trình, tiết kiệm không gian lưu trữ lại rất quan trọng, đặc biệt đối với
Trang 19những chương trình mà khi thực hiện cần dữ liệu nhập lớn do đó yêu cầu (3) sẽ được xem xét một cách kĩ lưỡng Ta gọi nó là hiệu quả của giải thuật Tính hiệu quả thể hiện qua hai mặt là thời gian và không gian
1.3.2 Thời gian thực hiện của giải thuật
Thời gian thực hiện của giải thuật (T) phụ thuộc vào nhiều yếu tố, tước hết phụ thuộc vào độ lớn của dữ liệu đầu vào, ngoài ra còn phụ thuộc vào máy tính, ngôn ngữ, kỹ sảo của người lập trình, Tuy nhiên các yếu tố này là không đồng đều do vậy không thể dựa vào chúng khi xác lập thời gian thực hiện giải thuật T(n), với n là kích thước của bài toán
a) Thời gian thực hiện giải thuật là gì
Thời gian thực hiện một chương trình là một hàm của kích thước dữ liệu vào, ký hiệu T(n) trong đó n là kích thước (độ lớn) của dữ liệu vào
Ví dụ 1: Chương trình tính tổng của n số có thời gian thực hiện là T(n) = c*n trong đó
Câu trả lời là có, thí dụ xét 2 giải thuật 1 (GT1) và giải thuật 2 (GT2) :
o GT1 có thời gian thực hiện là tỉ lệ với n
o GT2 có thời gian thực hiện là tỉ lệ với n 2
=> Với n khá lớn thì giải thuật 1 nhanh hơn giải thuật 2
Thời gian mà ta đánh giá như trên gọi là độ phức tạp tính toán của giải thuật:
Ở giải thuật 1 ta nói GT có độ phức tạp tính toán cấp n và kí hiệu T(n) = O(n)
Ở giải thuật 2 thì thời gian T() = O(n2) hay O(g(n)), g(n) còn được gọi là cấp độ phức tạp tính toán
Hàm g(n) thường chọn là: logn; n; n2; n3: ta gọi chung là hàm đa thức; g(n) = 2n
ta gọi là hàm mũ; các giải thuật có độ phức tạp tính toán là cấp các hàm đa thức thì chấp nhận được Ví dụ : T(n) = 60n2 + 9n + 9 = O(n)
Một số ví dụ đánh giá độ phức tạp tính toán của GT
Ví dụ 1 : Tính trung bình cộng của một dãy gồm n số nhập vào từ bàn phím
Trang 20T(n) = 4n + 4 =< 5n với mọi n >= 4 => T(n) = O(n)
Một số qui tắc cơ bản xác định độ phức tạp tính toán
a Qui tắc loại bỏ hằng số
Nếu ta có T(n) = O(C1.g(n)) thì ta cũng có T(n) = O(g(n))
b Qui tắc lấy max
Nếu ta có T(n) = O(f(n) + g(n)) thì ta cũng có T(n) = O(max(f(n),g(n)))
Tóm lại: Qui tắc tổng quát để phân tích một chương trình
Thời gian thực hiện của mỗi lệnh gán, READ, WRITE là O(1) Thời gian thực hiện của một chuỗi tuần tự các lệnh được xác định bằng qui tắc cộng Như vậy thời gian này
là thời gian thi hành một lệnh nào đó lâu nhất trong chuỗi lệnh Thời gian thực hiện cấu trúc IF là thời gian lớn nhất thực hiện lệnh sau THEN hoặc sau ELSE và thời gian kiểm tra điều kiện Thường thời gian kiểm tra điều kiện là O(1) Thời gian thực hiện vòng lặp là tổng thời gian thực hiện thân vòng lặp (trên tất cả các lần lặp) Nếu thời gian thực hiện thân vòng lặp không t h a y đổi thì thời gian thực hiện vòng lặp là
Trang 21tích của số lần lặp với thời gian thực hiện 1 lần thân vòng lặp
Ví dụ 1: Tính thời gian thực hiện của thủ tục sắp xếp “nổi bọt”:
void doicho(int*x, int*y)
//chuong trinh con sap xep
void Sapxep(int *a,int n)
- Trước hết, cả ba lệnh gán {4}, {5} và {6} đều tốn O(1) thời gian, việc so sánh cũng tốn O(1) thời gian, do đó lệnh {3} tốn O(1) thời gian
- Vòng lặp {2} thực hiện (n-i) lần, mỗi lần O(1) do đó vòng lặp {2} tốn i).1) = O(n-i)
O((n Vòng lặp {1} lặp có i chạy từ 1 đến nO((n 1nên thời gian thực hiện của vòng lặp {1} và cũng là độ phức tạp của giải thuật là:
Chú ý: Trong trường hợp vòng lặp không xác định được số lần lặp thì chúng ta phải
lấy số lần lặp trong trường hợp xấu nhất
Ví dụ 2: Xét giải thuật tìm kiếm tuần tự
Hàm tìm kiếm Search nhận vào một mảng a có n số nguyên và một số nguyên x, hàm sẽ trả về giá trị logic TRUE nếu tồn tại một phần tử a[i] = x, ngược lại hàm trả
về FALSE
Giải thuật tìm kiếm tuần tự: lần lượt so sánh x với các phần tử của mảng a, bắt đầu từ a[1], nếu tồn tại a[i] = x thì dừng và trả về TRUE, ngược lại nếu tất cả các phần tử của a đều khác X thì trả về FALSE
int Search(int a[], int x)
Trang 22return (found);
};
Ta thấy các lệnh {1}, {2}, {3} và {5} nối tiếp nhau, do đó độ phức tạp của hàm Search chính là độ phức tạp lớn nhất trong 4 lệnh này Dễ dàng thấy rằng ba lệnh {1}, {2} và {5} đều có độ phức tạp O(1) do đó độ phức tạp của hàm Search chính là độ phức tạp của lệnh {3} Lồng trong lệnh {3} là lệnh {4} Lệnh {4} có độ phức tạp O(1) Trong trường hợp xấu nhất (tất cả các phần tử của mảng a đều khác x) thì vòng lặp {3} thực hiện n lần, vậy ta có T(n) = O(n)
1.3.3 Không gian của giải thuật
Không gian của giải thuật, ký hiệu L(gt) được tính bằng số ô nhớ nguyên thuỷ được dùng trong giải thuật đó
Ví dụ: Xét giải thuật (gt1) tính trung bình cộng của một dãy gồm n số nhập vào từ bàn phím
7 Tính TBC = T/n;
8 Đưa ra TBC
=> L(gt1) = 4 (gồm biến n, biến d, biến T, biến TBC)
1.4 Ngôn ngữ diễn đạt giải thuật
Là công cụ trung gian giúp giao tiếp giữa người và MTĐT Mỗi ngôn ngữ lập trình có một hệ kiểu, trong đó có một số là kiểu dữ liệu đơn hay nguyên tử, một số là các cấu trúc dữ liệu chứa các kiểu đơn./cấu trúc khác
Ngôn ngữ diễn đạt giải thuật bao gồm một tập hợp các câu lệnh tuân theo một cú pháp nhất định Thông qua các câu lệnh mà MT có thể hiểu và thực hiện những công việc
mà người dùng muốn MT làm
Trang 231.5 Đệ quy và giải thuật đệ quy
1.5.1 Một số thuật ngữ
a) Lặp (interation): Là biến thể của cùng một thao tác
Ví dụ 1: Nhảy dây
Ví dụ 2: In ra màn hình các số từ 1 đến 10 For i:= 1 to n do writeln(i);
b) Quy nạp (induction): Là kĩ thuật chứng minh các mệnh đề thuộc dạng với "nếu n thì P(n) là đúng”
Ví dụ: với mọi n, tổng n số lẻ đầu tiên bằng n2
Các bước chứng minh:
Bước cơ sở: Chỉ ra P(1) là đúng , vì 12=1
Bước quy nạp: Chứng minh nếu P(n) là đúng thì kéo theo P(n+1) cũng đúng
c) Đệ quy (recursion): Là một kĩ thuật định nghĩa một khái niệm trực tiếp hoặc gián
tiếp theo chính nó
d) Đối tượng đệ quy: Một đối tượng được gọi là đệ quy nếu nó bao gồm chính nó như
một bộ phận hoặc được định nghĩa dưới dạng của chính nó
Ví dụ : Cho n là số nguyên dương, giai thừa của n được định nghĩa là :
n! = 1 nếu n = 0 hoặc n = 1;
n*(n-1) ! nếu n >1
Giai thừa của n được định nghĩa qua giai thừa của n-1, giai thừa của n-1 lại định nghĩa theo giai thừa của n-2, cứ tiếp tục như vậy tạo nên công thức đệ quy
1.5.2 Giải thuật đệ quy
a) Định nghĩa giải thuật đệ quy
Nếu lời giải P được thực hiện bằng lời giải P' có dạng như P thì ta nói đó là lời giải đệ quy
Giải thuật chứa lời giải đệ quy gọi là giải thuật đệ quy
Chú ý:
-P' giống P
-P' "nhỏ hơn" P theo nghĩa nào đó
b) Định nghĩa một hàm hay thủ tục đệ quy gồm hai phần:
(i) Phần neo (anchor): trường hợp đặc biệt (suy biến)
(ii) Phần đệ quy: Phần lặp gọi lại chính nó
Như vậy:
- Phần đệ quy thể hiện tính "quy nạp"
- Phần neo đảm bảo cho tính dừng
Trang 24Ví dụ 2: Xét giải thuật tìm một từ trong từ điển
void SEARCH( dict, word)
{dict được gọi là đầu mối để truy nhập được vào từ điển đang xét, word chỉ từ cần tìm }
1) if (từ điển chỉ còn là một trang )
{tìm word trong trang này}
else
{
- Mở từ điển vào trang giữa;
- Xác định xem nửa nào chứa word ;
- If (word nằm ở nửa trước của từ điển )
c) Đặc điểm của giải thuật đệ quy
(1) Trong giải thuật đệ quy bao giờ cũng có lời gọi đến chính teenn giải thuật (2) Mỗi lần có lời gọi thì kích thước của bài toán thu nhỏ hơn trước
(3) Có một trường hợp đặc biệt, trường hợp suy biến: Bài toán sẽ được giải quyết theo một cách khác hẳn và gọi đệ quy cũng kết thúc
Có 2 loại đệ quy, đệ quy trực tiếp (thủ tục chứa lời gọi đến chính nó) và đệ quy gián tiếp (thủ tục chứa lời gọi đến thủ tục khác mà thủ tục này lại chứa lời gọi đến chính nó)
1.5.3 Đánh giá về cách giải đệ quy
Cách giải đệ quy có ưu điểm là rõ ràng, chặt chẽ, thiết kế giải thuật đơn giản Tuy
nhiên nó cũng có những hạn chế là lời gọi đệ quy tốn rất nhiều thời gian, dễ phát sinh
chạy vô hạn
Trang 25CHƯƠNG 2 MÔ HÌNH DỮ LIỆU DANH SÁCH
2.1 Danh sách (List)
2.1.1 Khái niệm danh sách
Mô hình toán học của danh sách mô tả một tập hợp hữu hạn biến động các phần tử thuộc cùng một lớp đối tượng nào đó (có cùng một kiểu dữ liệu)
Lưu ý rằng một đối tượng cũng có thể xuất hiện nhiều lần trong một danh sách Ta biểu diễn danh sách L như là một chuỗi các phần tử của nó: a1, a2, , an với n ≥ 0 thì:
+ Nếu n = 0 ta nói danh sách rỗng (empty list)
+ Nếu n > 0 ta gọi a1 là phần tử đầu tiên và an là phần tử cuối cùng của danh sách
+ Số phần tử của danh sách ta gọi là độ dài của danh sách
+ Một tính chất quan trọng của danh sách đó là tính tuyến tính: Các phần tử của danh sách có thứ tự tuyến tính theo vị trí (position) xuất hiện của các phần tử
Ta nói ai đứng trước ai+1, với i từ 1 đến n-1; Tương tự ta nói ai là phần tử đứng sau ai-1, với i từ 2 đến n Ta cũng nói ai là phần tử tại vị trí thứ i, hay phần tử thứ i của danh sách
Ví dụ: Tập hợp họ tên các sinh viên của lớp TINHOC được liệt kê trên giấy như sau:
1 Nguyễn Trung Cang
2.1 2 Các phép toán cơ bản trên danh sách
Gọi L là danh sách, p là một vị trí (position) trong danh sách, x là một giá trị nào đó cùng kiểu với kiểu dữ liệu của các phần tử trong danh sách Các phép toán cơ bản thường gặp trên danh sách là:
1) Chèn một phần tử vào danh sách
INSERT_LIST (x,p,L): Chèn phần tử x vào vị trí p trong danh sách L Tức là
nếu danh sách là a1, a2, , ap-1, ap , , an thì sau khi xen ta có kết quả a1, a2, , a
p-1, x, ap, , an Nếu vị trí p không tồn tại trong danh sách thì phép toán không
Trang 26được xác định
2) Tìm vị trí của một phần tử trong danh sách
LOCATE(x,L) thực hiện việc xác định vị trí phần tử có nội dung x đầu tiên trong
danh sách L Hàm trả về kết quả là vị trí của phần tử x trong danh sách Nếu x không có trong danh sách thì vị trí sau phần tử cuối cùng của danh sách được trả về, tức là ENDLIST(L)
3) Lấy giá trị của phần tử ở vị trí nào đó
RETRIEVE(p,L) lấy giá trị của phần tử ở vị trí p của danh sách L; nếu vị trí p
không có trong danh sách thì kết quả không xác định (có thể thông báo lỗi)
4) Xoá một phần tử ở vị trí nào đó trong danh sách
DELETE_LIST(p,L): xoá phần tử ở vị trí p t r o n g danh sách L Nếu vị trí p
không có trong danh sách thì phép toán không được định nghĩa và danh sách L sẽ không thay đổi
5) Tìm vị trí của phần tử đứng sau phần tử có vị trí xác đinh
LINK(p,L) cho kết quả là vị trí của phần tử đi sau phần tử có vị trí p; nếu p là
phần tử cuối cùng trong danh sách L thì LINK(p,L) cho kết quả là ENDLIST(L) Link không xác định nếu p không phải là vị trí của một phần tử trong danh sách
6) Tìm vị trí của phần tử đứng trước phần tử có vị trí xác đinh
PREVIOUS(p,L) cho kết quả là vị trí của phần tử đứng trước phần tử có vị trí
p trong danh sách Nếu p là phần tử đầu tiên trong danh sách thì Previous(p,L) không xác định Previous cũng không xác định trong trường hợp p không phải là
vị trí của phần tử nào trong danh sách
7) Tìm vị trí của phần tử đứng đầu danh sách
FIRST(L) cho kết quả là vị trí của phần tử đầu tiên trong danh sách Nếu danh
sách rỗng thì ENDLIST(L) được trả về
8)Kiểm tra tính rỗng của danh sách
EMPTY_LIST(L) cho kết quả TRUE nếu danh sách rỗng, ngược lại nó cho giá
trị FALSE
9) Tạo một dách sách rỗng
MAKENULL_LIST(L) khởi tạo một danh sách L rỗng, chưa có dữ liệu
Trong thiết kế các giải thuật sau này chúng ta dùng các phép toán trừu tượng đã được định nghĩa ở trên đây như là các phép toán nguyên thủy (cơ bản) Thật vậy, từ các phép toán nguyên thuỷ này ta có thể tự hình thành lên các phép toán phức tạp khác thường gặp trên danh sách như tạo danh sách chứa dữ liêu, sắp xếp danh sách, duyệt danh sách, tách, gộp, tính toán, tổng hợp, hiện thị danh sách ….phụ thuộc vào yêu cầu
cụ thể của bài toán
Trên đây là các phép toán trừu tượng do chúng ta định nghĩa, nó chưa được cài đặt trong các ngôn ngữ lập trình Do đó để thực hiện được các phép toán đó ta phải cài đặt chúng thành các chương trình con bởi ngôn ngữ lập trình cụ thể
Trang 272.1.3 Biểu diễn danh sách trên máy tính
2.1.3.1 Danh sách cài đặt bằng mảng
Cài đặt danh sách bởi mảng hay còn gọi là CTDL danh sách đặc, hoặc CTDL danh sách
kế tiếp, gọi tắt là: Danh sách đặc, hoặc danh sách kế tiếp, nó thuộc loại cấu trúc dữ liệu tĩnh
a) Mô tả cài đặt
Giá sử N là số phân tử tối đa trong danh sách, với cách cài đặt này ta phải ước lượng
số phần tử tối đa của danh sách để khai báo số phần tử của mảng cho thích hợp Giả sử
Item là kiểu dữ liệu của các phần tử trong danh sách Khi đó ta sẽ biểu diễn danh sách
như sau:
Dùng một mảng để lưu giữ các phần tử của danh (giả sử mảng Elements)
Count là một biến đếm đếm số lượng phần tử hiện có trong danh sách
Như vậy ta có thể định nghĩa danh sách như một cấu trúc bản ghi gồm 2 trường:
Elements: Chứa các phần tử trong danh sách
Count: Đếm số phần tử hiện có trong danh sách (chiều dài danh sách)
Mảng chứa các phần tư trong danh sách có dạng:
typedef int item; // dinh nghia kieu item
// dinh nghia danh sach chua so nguyen
typedef struct List
{
item elems[N];
int size;
} List;
c) Cài đặt các phép toán cơ bản của danh sách
1- Khởi tạo danh sách rỗng
Danh sách rỗng là một danh sách không chứa bất kỳ một phần tử nào (hay độ dài danh sách bằng 0) Theo cách khai báo trên, trường count chỉ vị trí của phần tử cuối cùng trong danh sách và đó cũng độ dài hiện tại của danh sách, vì vậy để khởi tạo danh sách rỗng ta chỉ việc gán giá trị trường count này bằng 0
// phep toan khoi tao danh sach
void Init(List *L)
{
L->size=0;
}
2- Kiểm tra danh sách rỗng
Danh sách rỗng là một danh sách mà độ dài của nó bằng 0
// phep toan kiem tra ds rong
int IsEmpty( struct List L)
{
Trang 28if (L.size==0) return 1;
else return 0;
}
3- Thêm một phần tử vào danh sách
Xét danh sách L, một giá trị cần thêm x và một vị trí p bất kỳ trong danh sách Chèn x vào vị trí p trong danh sách L Khi chèn x vào vị trí p của danh sách L thì sẽ xuất hiện các khả năng sau:
Mảng đầy tức là mọi phần tử của mảng đều chứa phần tử của danh sách, tức là phần
tử cuối cùng của danh sách nằm n mảng Nói cách khác, độ dài của danh sách bằng chỉ số tối đa của mảng; khi đó không còn chỗ cho phần tử mới, vì vậy việc them là không thể thực hiện được, chương trình báo lỗi Ngược lại, nếu mảng chưa đầy ta tiếp tục xét vị trí p, nếu p không hợp lệ (p>count+1 hoặc p<1) thì chương trình báo lỗi; vì
vị trí thêm p<1 thì khi đó p không phải là một vị trí phần tử trong danh sách Nếu vị trí p>L.count+1 thì khi thêm sẽ làm cho danh sách L không còn là một danh sách đặc nữa Nếu vị trí p hợp lệ thì ta tiến hành thêm theo các bước sau:
- Dời các phần tử từ vị trí cuối danh sách đến vị trí p sang phải 1 ô nhớ
- Đưa phần tử mới vào vị trí p
- Tăng độ dài danh sách lên 1 đơn vị
Giải thuật thêm:
//Pheps toan them mot phan tu x, vao ds L, tai vi tri p, quy uoc p tinh tu
4 - Xóa phần tử ra khỏi danh sách
Xét danh sách L, hãy xoá phần tử ở vị trí p ra khỏi danh sách Để xoá phần tử ở vị trí p
ra khỏi danh sách L ta làm công việc ngược lại với thêm một phần tử: Trước tiên ta kiểm tra vị trí phần tử cần xóa xem có hợp lệ hay không Nếu p>L.count hoặc p<1 thì đây không phải là vị trí của phần tử trong danh sách Ngược lại, vị trí đã hợp lệ thì ta phải dời các phần tử từ vị trí p+1 đến cuối danh sách sang trái một ô nhớ để đè lên phần tử cần xóa và độ dài danh sách giảm đi 1 phần tử
Giải thuật xoá
// xoa 1 phan tu o vi tri p trong ds L, luu ptu xoa vao bien x
void Delete( struct List *L, int p, item *x)
Trang 29o Nếu tìm thấy x thì vị trí của phần tử tìm thấy được trả về,
o Nếu không tìm thấy thì hàm trả về vị trí sau vị trí của phần tử cuối cùng trong danh sách, tức là ENDLIST(L) Giả sử ta cho ENDLIST(L) := L.count+1
Trong trường hợp có nhiều phần tử cùng giá trị x trong danh sách thì vị trí của phần tử được tìm thấy đầu tiên trả về
6- Các phép toán khác cũng dễ dàng cài đặt xem như bài dành cho bạn đọc
2.1.3.2 Danh sách cài đặt bởi con trỏ
Danh sách được cài đặt bởi con trỏ ta còn gọi là CTDL danh sách liên kết hay CTDL
danh sách móc nối, gọi tắt là danh sách liên kết/danh sách móc nối Chúng thuộc loại cấu trúc dữ liệu động Ta đã biết với CTDL danh sách kế tiếp cài đặt bởi mảng, các ô nhớ chứa các phần tử trong danh sách là kế tiếp nhau, nằm trong một vùng nhớ liên tục, các ô nhớ này được cấp phát ngay khi dịch chương trình cho đến khi chương trình kết thúc thực hiện thì nó được giải phóng, các ô nhớ này ta còn gọi là các ô nhớ tĩnh
Có thể lập công thức tính địa chỉ và truy cập trực tiếp đến chúng nếu ta biết trước địa chỉ của một ô nhớ nào đó trong mảng Nhược điểm chính của cách biểu diễn này là số lượng các phần tử trong danh sách bị hạn chế vì phụ thuộc vào vùng nhớ trống liên tục trong bộ nhớ Xét một dạng biểu diễn danh sách khác, biểu diễn móc nối, các ô nhớ chứa các dữ liệu của danh sách không nhất thiết phải nằm ở những vị trí kế tiếp nhau, chúng có thể nằm rải rác khắp nơi trong bộ nhớ và chúng gắn kết với nhau thông qua
cơ chế móc nối – còn gọi là cơ chế lưu địa chỉ Danh sách được lưu trữ theo kiểu còn gọi là danh sách liên kết hay danh sách móc nối Trong phần nay ta tập trung vào các
CTDL danh sách liên kết Con trỏ được sử dụng để tạo liên kết giữa các ô nhớ chứa
phần tử của danh sách Các hình thức tổ chức liên kết các phần tử trong danh sách có thể là:
(1) Liên kết đơn, tương ứng ta có cấu trúc dữ liệu danh sách liên kết đơn – gọi tắt
là danh sách liên kết đơn
(2) Liên kết vòng: Tương ứng ta có cấu trúc dữ liệu danh sách liên kết vòng – gọi
tắt là danh sách liên kết vòng
(3) Liên kết đôi: Tương ứng ta có cấu trúc dữ liệu danh sách liên kết đôi – gọi tắt
là danh sách liên kết đôi/kép
(4)Đa liên kết: Tương ứng ta có danh sách đa liên kết/ đa móc nối
a) Danh sách liên kết đơn (Single Link List)
Mỗi phần tử trong danh sách là một ô nhớ, mỗi ô nhớ là một cấu trúc ít nhất là
Trang 30hai ngăn, một ngăn chứa dữ liệu của phần tử đó, một ngăn là con trỏ chứa địa chỉ của ô nhớ đứng kế sau phần tử này trong danh sách, ta có thể hình dung cơ chế này qua ví dụ sau:
Giả sử 1 nhóm có 4 bạn: Đông, Tây, Nam, Bắc có địa chỉ nhà ở lần lượt là d,t,n,b Giả sử: Đông có địa chỉ của Nam, Tây không có địa chỉ của bạn nào, Bắc giữ địa
chỉ của Đông, Nam có địa chỉ của Tây, điều này được mô tả qua Hình 2.1 như sau
Hình 2.1 – Danh sách liên kết đơn chứa 4 phần tử
Như vậy, nếu ta xét thứ tự các phần tử bằng cơ chế lưu địa chỉ này thì ta có một danh sách: Bắc, Đông, Nam, Tây Hơn nữa để có thể tru y cập đế n các phần tử trong danh sách này thì chỉ cần giữ địa chỉ của Bắc (địa chỉ của ô nhớ chứa phần
tử đầu tiên trong danh sách)
Mô tả dạng biểu diễn danh sách trên máy tính
Trong cài đặt, mỗi phần tử trong danh sách được cài đặt như một nút có hai trường:
Trường info chứa giá trị của các phần tử trong danh sách;
Trường link là một con trỏ giữ địa chỉ của ô kế tiếp nó trong danh sách:
Mỗi nút có dạng như sau:
Hình ảnh danh sách có dạng như sau:
Nút cuối cùng trong danh sách không có nút đứng sau, nên Trường link của phần tử
cuối trong danh sách, trỏ đến một giá trị đặc biệt là Nil (trỏ tới đất – không trỏ tới
Trang 31đâu) Để truy nhập vào d/s ta phải truy nhập tuần tự đến vị trí mong muốn, xuất phát
từ phần tử đầu tiên, do đó để quản lý danh sách ta chỉ cần quản lý địa chỉ ô nhớ chứa phần tử đầu tiên của danh sách, tức là cần một con trỏ trỏ đến phần tử đầu tiên này - giả sử con trỏ L L còn gọi là con trỏ quản lý danh sách Danh sách L rỗng khi: L=nil
Nói đến danh sách móc nối (Liên quan đến địa chỉ) ta phải nghĩ ngay đến biến trỏ, vậy biến trỏ là gì?
Dạng biểu diễn của danh sách:
typedef struct Node
{
int Data;
Node* Next;
}Node;
typedef Node* LinkedList;
* Cài đặt các phép toán cơ bản của danh sách liên kết đơn
1- Tạo danh sách rỗng
2- Kiểm tra một danh sách rỗng
3 -Chèn một phần tử vào danh sách:
Chèn một phần tử có giá trị x vào danh sách L tại vị trí p ta cần:
- Cấp phát một ô nhớ để lưu trữ phần tử mới này: Giả sử con trỏ Temp trỏ tới ô nhớ này
- Đổ dữ liệu cần chèn vào ô nhớ vừa cấp phát
- Nối kết lại các con trỏ để đưa ô nhớ mới này vào vị trí p Sơ đồ nối kết được
mô tả như trong Hình 2.2 Gồm:
o Di chuyển con trỏ M đến vị trí trước p
o Cho con trỏ Link của nút mới trỏ tới ô nhớ ứng với phần tử đứng sau nó (như thao tác 3)
o Xóa liên kết từ B đến C, và tạo liên kết từ phần tử thứ B đến phần tử mới ( như thao tác 4)
Hình 2.2: Thêm một phần tử vào danh sách tại vị trí p 4- Xóa phần tử ra khỏi danh sách L
Tương tự như khi thêm một phần tử vào danh sách liên kết, muốn xóa một phần
Trang 32(1) Xác định vị trí của phần tử muốn xóa trong danh sách L, giả sử ví trí thứ p, ta
di chuyển con trỏ M tới vị trí trước p, con trỏ temp trỏ tới vị trí p
(2) Nối kết lại các con trỏ theo thao tác 1 như trong Hình 2.3
(3) Giải phóng vùng nhớ chứa phần tử thứ P/được trỏ bởi Temp
Hình 2.3: Xoá phần tử tại vị trí p
5 – Nhập dữ liệu cho danh sách
void Nhapds(LinkedList &L) //nhap ds ket noi vao dau ds
{
Node *N;
int x;
int n; //bien n dung de chua so phan tu cua ds
L=NULL; //khoi tao ds ban dau rong
//Nhap so phan tu trong ds
printf( "Nhap so phan tu cua ds:" );
printf( "Nhap x=" ); scanf( "%d" ,&x);
6- In dữ liệu trong danh sách ra màn hình
//Chuong trinh inds ra man hinh
Trang 331- Danh sách nối vòng ( Circularly linked list)
Danh sách liên kết vòng là một cải tiến của d/s nối đơn Mỗi phần tử trong danh sách được lưu trong một nút, trường Link của nút cuối cùng trong d/s chứa địa chỉ của nút đầu tiên của d/s
Hình ảnh của nó như sau:
Ưu điểm của danh sách liên kết vòng giúp cho việc truy nhập vào các nút được linh hoạt hơn, vì nút nào trong danh sách cũng có thể coi là nút đầu tiên và con trỏ L trỏ tới nút nào cũng được, từ một nút trong danh sách ta có thể truy cập được đến các nút khác, tuy nhiên trong xử lý, nếu không cẩn thận sẽ dẫn đến 1 chu trình không kết thúc (Vì không biết được chỗ kết thúc d/s)
2- Danh sách nối kép (double link list)
Chúng ta nhận thấy rằng với danh sách móc nối đơn và nối vòng, chỉ có phép duyệt 1 chiều, từ phần tử trước có thể truy nhập đến phần tử đứng sau, nhưng từ phần tử đứng sau không truy cập trực tiếp đến phần tử đứng ngay trước nó được Khắc phục hạn chế này ta có danh sách liên kết kép Mỗi phần tử trong danh sách nối kép xem là một nút (bản ghi) gồm 3 trường:
- Info : chứa thông tin về đối tượng
- LPTR : con trỏ trỏ tới phần tử bên trái
- RPTR : con trỏ trỏ tới phần tử bên phải
Quy cách một nút:
Hình ảnh danh sách móc nối kép có dạng:
Việc truy nhập các phần tử của d/s phải truy cập xuất phát từ một trong hai đầu của danh sách Do đó, để quản lý danh sách, dùng 2 con trỏ L, R lần lượt trỏ tới nút trái nhất và phải nhất của danh sách, hai con trỏ này còn gọi là con trỏ cực trái, cực phải của danh sách Khi đó d/s rỗng nếu: L=R= nill
Biểu diễn danh sách liên kết vòng
Hình 2.4 – hình ảnh danh sách liên kết đơn vòng
Hình 2.5 – hình ảnh danh sách liên kết đôi
Trang 34Các bước để chèn thêm một phần tử mới vào danh sách liên kết kép được thực hiện tương tự như danh sách liên kết đơn, tuy nhiên khi gắn kết nút tương ứng với phần tử mới (nút được trỏ bởi con trỏ q) vào vị trí trước phần tử được trở bởi con trỏ M được thực hiện từ bước (1) đến (4) như mô tả như trong hình 2.6:
2) Phép loại bỏ 1 phần tử ra khỏi danh sách nối kép
Xét d/s nối kép, có 2 nút cực trái, cực phải là L, R Loại bỏ nút trỏ bởi con trỏ M ra khỏi danh sách Để loại bỏ phần tử được trỏ bởi con trỏ M ra khỏi danh sách liên kết kép L, R ta tiến hành theo các bước từ 1) đến 5) như sau:
1) Nếu danh sách rỗng, thông báo lỗi không có dữ liệu để loại bỏ
2) Việc xóa dữ liệu trong danh sách cần chia thành các trường hợp:
a Nếu M =L: phần tử loại bỏ là phần tử đầu tiên
b Nếu M =R: phần tử loại bỏ là phần tử cuối
c Nếu phần tử loại bỏ nằm ở vị trí p trong danh sách 3) Cuối cùng ta thực hiện thao tác giải phóng vùng nhớ chứa phần tử cần loại bỏ
Việc loại bỏ ứng với trường hợp (c) được mô tả như trong hình 2.7
2.1.3.3 So sánh hai phương pháp cài đặt danh sách bởi mảng và bởi con trỏ
Không thể kết luận phương pháp cài đặt nào hiệu quả hơn, mà nó phụ thuộc chủ yếu vào bài toán và các phép toán trên danh sách Tuy nhiên ta có thể tổng kết một số ưu nhược điểm/ nhược điểm chung của từng phương pháp làm cơ sở để lựa chọn phương pháp biểu diễn thích hợp:
Trang 3535
Cài đặt bằng mảng đòi hỏi phải xác định số phần tử của mảng, do đó nếu không thể ước lượng được số phần tử trong danh sách thì khó áp dụng cách cài đặt này một cách hiệu quả vì nếu khai báo thiếu chỗ thì mảng thường xuyên bị đầy, không thể làm việc được còn nếu khai báo quá thừa thì lãng phí bộ nhớ
Cài đặt bằng con trỏ thích hợp cho sự biến động của danh sách, danh sách có thể rỗng hoặc lớn tuỳ ý chỉ phụ thuộc vào bộ nhớ tối đa của máy Tuy nhiên ta phải tốn thêm vùng nhớ cho các con trỏ
Cài đặt bằng mảng thì thời gian thêm hoặc xoá một phần tử tỉ lệ với số phần tử
đi sau vị trí thêm/ xóa Trong khi cài đặt bằng con trỏ các phép toán này mất chỉ một hằng thời gian
Phép truy nhập vào một phần tử trong danh sách, chẳng hạn như PREVIOUS, chỉ tốn một hằng thời gian đối với cài đặt bằng mảng, trong khi đối với danh sách cài đặt bằng con trỏ ta phải tìm từ đầu danh sách cho đến vị trí trước vị trí của
phần tử hiện hành Nói chung danh sách liên kết thích hợp với danh sách có
nhiều biến động, tức là ta thường xuyên thêm, xoá các phần tử
2.2 Ngăn xếp (Stack)
2.2.1 Định nghĩa ngăn xếp
Ngăn xếp (Stack) là một danh sách đặc biệt, trong đó việc thêm vào hoặc loại bỏ một phần tử chỉ thực hiện tại một đầu của danh sách, đầu này gọi là đỉnh (TOP) của ngăn xếp
Ví dụ, có thể xem hình ảnh trực quan của ngăn xếp bằng một chồng đĩa đặt trên bàn Muốn thêm vào chồng đó 1 đĩa ta để đĩa mới trên đỉnh chồng, muốn lấy các đĩa ra khỏi chồng ta cũng phải lấy đĩa trên đỉ nh trước Như vậy ngăn xếp là một cấu trúc
có tính chất “vào sau - ra trước” hay “vào trước – ra sau“ (LIFO: Last In - First Out hay FILO: First In – Last Out) Hình ảnh của ngăn xếp có dạng (hình 2.8 a ), việc lấy
một phần tử ra khỏi ngăn xếp mô tả như hình 2.8.b, việc thêm một phần tử vào ngăn xếp được mô tả như hình 2.8 c:
Trang 362.2.2 Các phép toán cơ bản trên ngăn xếp:
1 - MAKENULL_STACK(S): tạo một ngăn xếp rỗng
2 - POP(S,x): Lấy một phần tử tại đỉnh ngăn xếp S lưu vào biến x
3 - PUSH(x,S): Thêm phần tử x vào đầu ngăn xếp S
4 - EMPTY_STACK(S): kiểm tra ngăn xếp S có rỗng không Hàm cho
kết quả true nếu ngăn xếp rỗng và false trong trường hợp ngược lại
? Ta có thể truy xuất trực tiếp phần tử tại vị trí bất kỳ trong ngăn xếp được không, nếu được thì chỉ ra cách làm
2.2.3 Biểu diễn ngăn xếp trên máy tính
a) Biểu diễn ngăn xếp
Tương tự danh sách, ngăn xếp có thể biểu diễn bởi mảng hoặc con trò
Ví dụ: Biểu diễn ngăn xếp bởi con trỏ
#include <stdio.h>
#define N 100
typedef int item;
typedef struct Stack
2 - Kiểm tra ngăn xếp rỗng, tính đầy của ngăn xếp:
// pheps toan kiem tra ngan xep rong
Trang 37// Day 1 phan tu x vao ngan xep S
int push(item x, Stack *S)
4 – Lấy 1 phần tử ra khỏi ngăn xếp S lưu và biến x để xử lý:
// Nhac 1 ptu ra khoi ngan xep
int pop(Stack *S, item *x)
môi trường của các chương trình con này Ngoài ra nó còn được sử dụng để khử đệ
quy/loại bỏ tính đệ qui của chương trình
Ví dụ, nếu một chương trình con đệ qui P(x) được gọi từ chương trình chính, ta nói chương trình con được thực hiện ở mức 1 Chương trình con này gọi chính nó, ta nói nó đi sâu vào mức 2 cho đến một mức k Rõ ràng mức k phải thực hiện xong thì
Trang 38mức k-1 mới được thực hiện tiếp tục, hay ta còn nói là chương trình con quay về mức k-1 Trong khi một chương trình con từ mức i đi vào mức i+1 thì các biến cục bộ của mức i và địa chỉ của mã lệnh còn dang dở phải được lưu trữ, địa chỉ này gọi là địa chỉ trở về Khi từ mức i+1 quay về mức i các giá trị đó được sử dụng Như vậy những biến cục bộ và địa chỉ lưu sau được dùng trước Tính chất này gợi ý cho ta dùng một ngăn xếp để lưu giữ các giá trị cần thiết của mỗi lần gọi tới chương trình con Mỗi khi lùi về một mức thì các giá trị này được lấy ra để tiếp tục thực hiện mức này Ta có thể tóm tắt quá trình như sau:
Bước 1: Lưu các biến cục bộ và địa chỉ trở về
Bước 2: Nếu thoả điều kiện ngừng đệ qui thì chuyển sang bước 3 Nếu không
thì tính toán từng phần và quay lại bước 1 (đệ qui tiếp)
Bước 3: Khôi phục lại các biến cục bộ và địa chỉ trở về
2.3 Hàng đợi (QUEUE)
2.3.1 Định nghĩa hàng đợi
Hàng đợi, hay ngắn gọn là hàng (queue) là một danh sách đặc biệt mà phép thêm một phần tử vào hàng chỉ thực hiện tại một đầu của hàng, gọi là cuối hàng (REAR), còn phép lấy một phần tử ra khỏi hàng thì thực hiện ở đầu còn lại của hàng, gọi là đầu hàng (FRONT)
Ví dụ, khi ta xếp hàng mua vé xem phim là một hình ảnh trực quan của khái niệm trên, người mới đến thêm vào cuối hàng còn người ở đầu hàng mua vé và ra khỏi
hàng, vì vậy hàng còn được gọi là cấu trúc hoạt động theo nguyên tắc FIFO: First In
- First Out, hay theo nguyên tắc "vào trước - ra trước, vào sau – ra sau"
Hình ảnh của hàng có dạng:
Ak Ak+1 Ak+2 Ak+3
2.3.2 Các phép toán cơ bản trên hàng
- MAKENULL_QUEUE(Q): khởi tạo một hàng rỗng
- FRONT(Q): trả về phần tử đầu tiên của hàng Q
- INSERT_QUEUE(x,Q): thêm phần tử x vào cuối hàng Q
- DEQUEUE(Q): xoá phần tử tại đầu của hàng Q
- EMPTY_QUEUE(Q): kiểm tra hàng Q có rỗng không
- FULL_QUEUE(Q): kiểm tra hàng Q có đầy không
2.3.3 Biểu diễn hàng đợi
Như đã trình bày trong phần ngăn xếp, ta hoàn toàn có thể dùng danh sách để biểu diễn cho một hàng và dùng các phép toán đã được cài đặt của danh sách để cài đặt các phép toán trên hàng Tuy nhiên làm như vậy có khi sẽ không hiệu quả, chẳng
Hình 2.9 – Hình ảnh hàng