1. Trang chủ
  2. » Công Nghệ Thông Tin

Giáo trình Cấu trúc dữ liệu và giải thuật (Nghề: Quản trị mạng - Trung cấp) - Trường Cao đẳng Cơ điện Xây dựng Việt Xô

73 4 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 73
Dung lượng 1,02 MB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Cấu trúc

  • CHƯƠNG 1: TỔNG QUAN VỀ CẤU TRÚC DỮ LIỆU VÀ GIẢI THUẬT (8)
    • 1.1. Khái niệm giải thuật (8)
    • 1.2. Ngôn ngữ diễn đạt giải thuật (8)
    • 1.3. Thiết kế giải thuật (12)
    • 1.4. Đánh giá giải thuật (14)
    • 3.1. Kiểu bản ghi (17)
    • 3.2. Kiểu con trỏ (17)
    • 5.1. Mảng (18)
    • 5.2. Danh sách liên kết (20)
  • CHƯƠNG 2: ĐỆ QUY VÀ GIẢI THUẬT ĐỆ QUY (25)
    • 2.1. Giải thuật đệ qui (25)
    • 2.2. Chương trình đệ qui (25)
    • 3.1. Bài toán tính n giai thừa (26)
    • 3.2. Bài toán dãy số FIBONACCI (26)
  • CHƯƠNG 3: DANH SÁCH (29)
    • 1.1. Khái niệm danh dách (29)
    • 1.2. Các phép toán trên danh dách (29)
    • 2.1. Khởi tạo danh sách rỗng (30)
    • 2.2. Kiểm tra danh sách rỗng (30)
    • 2.3. Chèn phần tử vào danh sách (30)
    • 2.4. Xóa phần tử khỏi danh sách (31)
    • 3.1. Khởi tạo danh sách rỗng (33)
    • 3.2. Kiểm tra danh sách rỗng (33)
    • 3.3. Chèn phần tử vào danh sách (33)
    • 3.4. Xóa phần tử khỏi danh sách (34)
    • 3.5. Danh sách liên kết vòng (35)
    • 3.6. Danh sách liên kết đôi (36)
    • 4. Danh sách đặc biệt (36)
      • 4.1. Ngăn xếp (36)
      • 4.2. Hàng đợi (40)
  • CHƯƠNG 4: CÁC PHƯƠNG PHÁP SẮP XẾP CƠ BẢN (45)
    • 2. Phương pháp chọn (Selection sort) (45)
      • 2.1. Giới thiệu phương pháp (45)
      • 2.2. Giải thuật (45)
      • 2.3. Ví dụ minh họa (46)
    • 3. Phương pháp chèn (Insertion sort) (47)
      • 3.1. Giới thiệu phương pháp (47)
      • 3.2. Giải thuật (47)
      • 3.3. Ví dụ minh họa (48)
    • 4. Phương pháp đổi chỗ (Interchange sort) (48)
      • 4.1. Giới thiệu phương pháp (48)
      • 4.2. Giải thuật (48)
      • 4.3. Ví dụ minh họa (49)
      • 5.1. Giới thiệu phương pháp (49)
      • 5.2. Giải thuật (49)
      • 5.3. Ví dụ minh họa (50)
      • 6.1. Giới thiệu phương pháp (51)
      • 6.2. Giải thuật (51)
      • 6.3. Ví dụ minh họa (52)
  • CHƯƠNG 5: TÌM KIẾM (54)
    • 1.1. Giới thiệu phương pháp (54)
    • 1.2. Giải thuật (54)
    • 1.3. Ví dụ minh họa (55)
  • CHƯƠNG 6: CÂY (58)
    • 1. Khái niệm về cây và cây nhị phân (58)
      • 1.1. Các khái niệm về cây (58)
      • 1.2. Khái niệm cây nhị phân (59)
    • 2. Biểu diễn cây nhị phân và cây tổng quát (59)
      • 2.1. Biểu diễn cây nhị phân (59)
      • 2.2. Biểu diễn cây tổng quát (62)
    • 3. Bài toán duyệt cây nhị phân (64)
      • 3.1. Duyệt theo thứ tự trước (gốc – trái – phải) (64)
      • 3.2. Duyệt theo thứ tự giữa (trái – gốc – phải) (64)
      • 3.3. Duyệt theo thứ tự sau (trái – phải – gốc) (65)
  • CHƯƠNG 7: ĐỒ THỊ (66)
    • 2. Biểu diễn đồ thị (67)
      • 2.1. Biểu diễn đồ thị bằng ma trận kề (67)
      • 2.2. Biểu diễn đồ thị bằng danh sách các đỉnh kề (67)
    • 3. Bài toán tìm đường đi trên đồ thị (68)

Nội dung

Giáo trình Cấu trúc dữ liệu và giải thuật (Nghề: Quản trị mạng - Trung cấp) kết cấu gồm 7 chương, cung cấp cho học viên những kiến thức về: tổng quan về cấu trúc dữ liệu và giải thuật; đệ qui và giải thuật đệ qui; danh sách; các phương pháp sắp xếp cơ bản; tìm kiếm; cây; đồ thị;... Mời các bạn cùng tham khảo!

TỔNG QUAN VỀ CẤU TRÚC DỮ LIỆU VÀ GIẢI THUẬT

Khái niệm giải thuật

Giải thuật, còn gọi là thuật toán, là một trong những khái niệm quan trọng nhất trong tin học Thuật ngữ thuật toán xuất phát từ tên của nhà toán học Ả Rập Abu Ja'far Mohammed ibn Musa al-Khwarizmi (khoảng năm 825).

Giải thuật thể hiện một giải pháp cụ thể, thực hiện từng bước một để đưa tới lời giải cho một bài toán

Giải thuật là một tập hợp hữu hạn các phép toán cơ bản được sắp xếp theo các quy tắc rõ ràng nhằm giải quyết một bài toán, hoặc là một bộ quy tắc và quy trình cụ thể để xử lý một vấn đề qua một chuỗi các bước hữu hạn, với mục tiêu cho ra kết quả từ tập dữ liệu đầu vào.

Các phép toán cơ sở là các thao tác cơ bản trong xử lý dữ liệu, có thời gian thực hiện hữu hạn và không phụ thuộc vào kích thước dữ liệu đầu vào Điều này cho thấy chi phí tính toán của mỗi phép toán cơ sở được xác định bởi bản chất của chính phép toán đó, bất kể dữ liệu lớn hay nhỏ Không chỉ vậy, đặc tính này làm cho các phép toán cơ sở trở thành nền tảng cho phân tích độ phức tạp thời gian và thiết kế thuật toán tối ưu, giúp đảm bảo hiệu suất ổn định và khả năng mở rộng khi xử lý dữ liệu ở quy mô lớn.

Các phép toán trong giải thuật phải đƣợc xác định rỏ ràng, dễ hiểu, không mập mờ

Với mọi bộ dữ liệu vào thoả mãn các điều kiện của bài toán, thuật toán phải dừng lại sau một số hữu hạn các bước cần thực hiện

Ngôn ngữ diễn đạt giải thuật

- Giả ngữ, là một ngôn ngữ ”tựa ngôn ngữ lập trình”

- Ngôn ngữ lập trình (Pascal, C, ) Trong tài liệu này chúng ta sử dụng ngôn ngữ tựa Pascal để trình bày Sau đây là một số qui tắt cơ bản:

1.2.1 Quy cách về cấu trúc chương trình

Mỗi chương trình được gán một tên duy nhất để phân biệt, tên này được viết bằng chữ in hoa, có thể có dấu gạch nối và bắt đầu bằng từ khóa Program, nhằm tăng tính nhận diện và tính nhất quán cho danh mục Quy ước đặt tên này cũng hỗ trợ tối ưu hóa SEO bằng cách tạo các cụm từ khóa dễ nhớ và dễ được tìm thấy bởi người dùng và máy tìm kiếm.

Ví dụ : Prorgram NHAN-MA-TRAN Độ dài tên không hạn chế

Đối với tên giải thuật, có thể kèm theo lời thuyết minh bằng Tiếng Việt nhằm giới thiệu tóm tắt nhiệm vụ của giải thuật và một số chi tiết cần thiết Phần thuyết minh được đặt giữa hai dấu { } để phân biệt rõ ràng với phần tên, giúp người đọc nắm bắt nhanh ý định và tăng khả năng tối ưu hóa công cụ tìm kiếm (SEO).

Chương trình bao gồm nhiều bước, mỗi bước được phân biệt bởi số thứ tự, có thể kèm theo những lời thuyết minh

1.2.2 Kí tự và biểu thức

Kí tự dùng ở đây cũng giống nhƣ trong các ngôn ngữ chuẩn, nghĩa là gồm :

26 chữ cái Latinh in hoa hoặc in thường

Các dấu phép toán số học: +, - , *, /, (lũy thừa)

Các dấu phép toán quan hệ: , , , #

Giá trị logic: true, false

Dấu phép toán logic: and, or, not

Tên biến là dãy chữ cái và chữ số, bắt đầu bằng chữ cái

Biến chỉ số có dạng : A[i], B[ij] v.v

Còn biểu thức cũng nhƣ thứ tự ƣu tiên của các phép toán trong biểu thức cũng theo quy tắc nhƣ trong PASCAL hay các ngôn ngữ chuẩn khác

Các câu lệnh trong chương trình được viết cách nhau bởi dấu chấm phảy chúng bao gổm :

Có dạng Tên biến/ Tên hàm : = Biểu thức Ở đây cho phép dùng phép gán chung

Có dạng : begin Câu lệnh 1 ; Câu lệnh2 ; ; Câu lệnhn end

Nó cho phép ghép nhiều câu lệnh lại để đƣợc coi nhƣ một câu lệnh

Có dạng : if < Điều kiện > then < Câu lệnh >

Có thể diễn tả bởi sơ đồ :

Hoặc if < Điều kiện > then < Câu lệnh1 > else < Câu lệnh2 > Điều kiện Câu lệnh 1 false true

Case Điều kiện 1 : Câu lệnh1; Điều kiện2: Câu lệnh2;

End case là một kỹ thuật trong lập trình giúp phân biệt các tình huống xử lý dựa trên các điều kiện khác nhau mà không cần sử dụng nhiều câu lệnh if-then-else Cơ chế này cho phép xác định luồng thực thi cho từng trường hợp một cách rõ ràng và ngắn gọn, tối ưu hóa logic điều kiện và giảm độ phức tạp của mã nguồn End case có thể được diễn đạt bằng sơ đồ hoặc sơ đồ luồng để hình dung các nhánh điều kiện và kết quả mong đợi ở mỗi tình huống.

Vài điểm linh động esle có thể không có mặt

Câu lệnh i (i = 1, 2, …, n) có thể đƣợc thay thế bằng một dãy các câu lệnh mà không cần phải đặt giữa : begin và end

Với số lần lặp đã được xác định trước, bạn có thể dùng vòng lặp for để thực thi một câu lệnh với i từ m tới n (n ≥ m) với bước nhảy bằng 1: for i = m to n do Ngược lại, bạn có thể lặp ngược từ n về m với for i := n down to m do , với bước nhảy bằng 1 để i giảm dần Hai kiểu vòng lặp này giúp kiểm soát số lần lặp và phạm vi giá trị của i, tối ưu hoá hiệu suất và đảm bảo câu lệnh được thực thi đúng cách trên mỗi giá trị của i Việc nêu rõ phạm vi và hướng lặp là khía cạnh quan trọng khi viết vòng lặp for trong các ngôn ngữ lập trình phổ biến, giúp người học nắm vững cú pháp và logic lặp lại.

Với số lần lặp không biết trước:

While < Điều kiện > do < Câu lệnh>

S 1 S 2 S n false false tru e tru e tru e Chú thích:

Câu lệnh Điều kiện true false

Chừng nào mà < Điều kiện > có giá trị bằng true thì thực hiện < Câu lệnh> Hoặc : repeat < Câu lệnh> until < Điều kiện >

Lặp lại < Câu lệnh> cho tới khi < Điều kiện > có giá trị true

Câu lệnh nhập: read ()

Câu lệnh xuất: write() các biến trong danh sách cách nhau bởi dấu phẩy

Dòng kí tự là một dãy các kí tự đặt giữa hai dấu nháy‟ „

Câu lệnh kết thúc chương trình: End

Function ()

Chương trình con thủ tục

Function ()

Câu lệnh kết thúc chương trình ở đây là return thay cho end

Trong cấu trúc của chương trình con, hàm luôn có một câu lệnh gán với tên hàm ở vế trái để trả về giá trị, còn ở chương trình con thủ tục thì không có câu lệnh gán như vậy vì thủ tục không trả về giá trị.

Trong lập trình, lời gọi chương trình con hàm được thể hiện bằng tên hàm kèm danh sách tham số thực sự nằm ngay trong biểu thức gọi, cho phép xác định giá trị trả về và luồng thực thi một cách rõ ràng Ngược lại, lời gọi chương trình con thủ tục được biểu diễn bằng câu lệnh gọi (call) với cú pháp cố định của ngôn ngữ, thường có dạng tên_thủ_tục(tham số) hoặc cú pháp call tương ứng, nhằm điều khiển luồng thực thi một cách có cấu trúc Trong quá trình điều khiển, các điều kiện và câu lệnh liên quan xác định nhánh thực thi, với các giá trị true và false quyết định câu lệnh nào được thực thi tiếp theo.

Câu lệnh Điều kiện fasle true

Call ()

Chú ý: Trong các chương trình diễn đạt một giải thuật, phần khai báo dữ liệu được bỏ qua và thay bằng phần mô tả cấu trúc dữ liệu bằng ngôn ngữ tự nhiên Mô tả này được nêu trước khi bước vào giải thuật, giúp người đọc hình dung các thành phần dữ liệu và cách chúng liên kết với các thao tác xử lý Việc trình bày cấu trúc dữ liệu dưới dạng ngôn ngữ tự nhiên thay cho khai báo dữ liệu giúp bài giải thuật dễ theo dõi và tối ưu hóa cho SEO bằng cách làm rõ ý tưởng và các từ khóa liên quan.

Thiết kế giải thuật

Tạo lập giải thuật để giải một bài toán là một nghệ thuật mà không bao giờ có thể nêu đầy đủ ngay một lúc

Có nhiều phương pháp thiết kế thuật toán khác nhau Tuy nhiên, mọi thứ sẽ dễ dàng hơn khi ta có thể phân chia bài toán lớn thành các bài toán nhỏ hơn, điều này đồng nghĩa ta coi bài toán là một mô-đun chính cần phân chia thành các mô-đun con, và theo tinh thần ấy đến các mô-đun con cho tới khi từng mô-đun đủ nhỏ để có thể xử lý trực tiếp Sau đó chỉ cần tổng hợp kết quả từ các phép xử lý ở các mô-đun con để có giải thuật cho bài toán gốc.

Xác định rõ dữ liệu và yêu cầu là bước đầu quan trọng để phân biệt dữ liệu input và dữ liệu output Cần làm rõ cái gì được cung cấp làm dữ liệu input và cái gì được yêu cầu trả về dưới dạng dữ liệu output Để giải quyết yêu cầu, bước tiếp theo là xác định phải làm gì và quy trình xử lý dữ liệu; tuy nhiên ở giai đoạn này ta mới chỉ phân tích được các câu hỏi liên quan đến dữ liệu output và cách biến đổi từ input thành output.

Với mỗi công việc ấy thì “ phải làm thê nào “ ?

Trên cơ sở đó mới cụ thể hóa dần dần các phép xử lí để xây dựng giải thuật cần thiết

Tất nhiên, khi giải quyết câu hỏi “ làm thế nào ?” thì dữ liệu input cũng phải đƣợc định hình về cấu trúc

Ví dụ, ta xét bài toán :

Sắp xếp là một dãy số ( a1,a2,….,an) thành một dãy số tăng dần

 Nhƣ vậy dãy số input, nếu có dạng, chẳng hạn :

(33, 77, 11, 55, 99, 22, 44, 88, 66) thì dãy số output phải có dạng :

 Để có đƣợc kết quả output nhƣ vậy thì phải làm gì ?

Có thể thấy rằng : sắp xếp theo thứ tự tăng dần nghĩa là :

– Số bé nhất trong n số phải đƣợc đặt vào vị trí đầu tiên ;

– Số bé nhất trong (n – 1 ) số còn lại phải đƣợc đặt vào vị trí thứ hai ; v.v… Nhƣ vậy sẽ có hai công việc chính phải làm :

 Chọn số bé nhất trong dãy số chƣa đƣợc sắp

 Đặt nó vào vị trí sau phần tử cuối của dãy số đã đƣợc sắp ( nó lại trở thành phần tử cuối cho bước tiếp theo )

Chú ý rằng : lúc đầu dãy số đƣợc sắp còn rỗng, sau đó nó đƣợc bổ sung dần dần các phần tử vào

Các công việc trên sẽ đƣợc lặp lại (n - 1) lần : đầu với n số, lần cuối với 2 số

 Để thực hiện đƣợc hai công việc nêu trên thì phải “làm thế nào ? ”

Trước hết, ta cần đặt câu hỏi nền tảng: dãy số này được định hình theo cấu trúc dữ liệu nào và được triển khai trong máy theo cấu trúc lưu trữ nào? Việc xác định rõ cấu trúc dữ liệu và cấu trúc lưu trữ sẽ giúp hiểu cách quản lý và tối ưu hóa dãy số trong bộ nhớ, từ đó cải thiện hiệu suất xử lý và truy cập dữ liệu.

Thông thường hệ thống được định hình và triển khai theo cấu trúc vectơ Ở đây có hai vectơ cơ bản: vectơ input và vectơ output Vậy trong máy nên dùng hai vectơ để lưu trữ dữ liệu hay chỉ dùng một vectơ duy nhất?

Giả sử ta chỉ dùng một vector duy nhất, nghĩa là lúc đầu vector lưu trữ dãy số cần xử lý, nhưng sau khi thực hiện thuật toán, chính vector đó lại chứa dãy số đã được sắp xếp, nhằm tiết kiệm bộ nhớ bằng cách tối ưu hoá cách lưu trữ dữ liệu mà không cần sao chép thêm cấu trúc.

Nếu thế thì công việc “đổi chỗ” sẽ đƣợc cụ thể thêm nhƣ sau :

Trong thuật toán sắp xếp chọn, ta hoán đổi vị trí của số bé nhất vừa được chọn với vị trí ở đầu dãy chưa được sắp, sau đó số bé nhất này được gỡ khỏi phần chưa sắp và trở thành phần tử cuối cùng của dãy đã được sắp xếp.

Tới đây ta có thể diễn ddajt sơ bộ giải thuật “sắp xếp” của ta nhƣ sau :

{A là vectơ gồm n phần tử là các số cho}

1.{2 công việc đƣợc lặp lại (n-1) lần} for i:=1 to (n-1) do begin

2.Chọn số nhỏ nhất A[k] trong dãy các số:

Bây giờ ta đi sâu vào từng công việc :

 Làm thế nào để chọn đƣợc số nhỏ nhất trong dãy các số:

Để tìm phần tử nhỏ nhất, ta bắt đầu bằng cách chọn một phần tử A[i], sau đó so sánh các phần tử tiếp theo với nó Nếu gặp phần tử nhỏ hơn, ta cập nhật A[i] bằng phần tử đó Quá trình này được lặp lại cho đến khi duyệt hết danh sách; phần tử được cập nhật cuối cùng chính là phần tử cần tìm.

Trong bài toán tìm phần tử nhỏ nhất của một mảng, ta chỉ cần lưu lại chỉ số k của phần tử nhỏ nhất thay vì lưu toàn bộ giá trị Cách diễn đạt ngắn gọn như sau: k := i; { coi phần tử đầu là nhỏ nhất lúc đó, và giữ lại chỉ số của nó } for j := i+1 to n do if A[j] < A[k] then k := j Nhờ đó, kết quả là chỉ số của phần tử nhỏ nhất trong đoạn A[i n], và bước "chọn" ở đây chỉ dựa trên việc so sánh hai giá trị tại các vị trí k và j.

 Làm thế nào để thực hiện đƣợc việc hoán vị chỗ cho hai phần tử ?

Cách giải bài này được minh họa bằng hình ảnh hai cốc khác nhau: một cốc chứa rượu, một cốc chứa nước Mục tiêu là hoán vị hai chất lỏng này, tức là đổi chỗ: chuyển rượu sang cốc đang đựng nước và ngược lại nước sang cốc đang đựng rượu Quá trình hoán đổi này cho thấy cách chuyển trạng thái của hai chất lỏng bằng các thao tác logic đơn giản tùy vào ngữ cảnh bài toán.

Rõ ràng điều này chỉ có thể thực hiện đƣợc khi ta dùng tới một cóc thứ ba làm cốc trung chuyển

Từ đó ta có thể diễn đạt việc hoán vị giữa A[k] và A[i] nhƣ sau :

Tổng hợp những ghi nhận ở trên , ta đi tới một thủ tục , thể hiện giải thuật

“sắp xếp” của ta ,bằng ngôn ngữ tựa PASCAL nhƣ sau :

 Cách làm ở trên phản ảnh một phương pháp thiết kế giải thuật, gắn liền với lập trình được gọi là "phương pháp tinh chỉnh từng bước" (stepwise refinement)

Cách cài đặt một cấu trúc dữ liệu trong máy tính điện tử có thể khác nhau giữa các hệ thống Vì vậy, để phân biệt, ta gọi cấu trúc cài đặt trong máy của một hệ thống là triển khai của cấu trúc dữ liệu đó.

“cấu trúc dữ liệu” là “cấu trúc lưu trữ” Như vậy nghĩa là cấu trúc lưu trữ có thể biểu diễn được nhiều cấu trúc dữ liệu khác nhau.

Đánh giá giải thuật

Khi giải quyết một vấn đề, chúng ta chọn trong số các thuật toán một thuật toán được cho là tốt nhất Để quyết định, ta dựa trên hai tiêu chuẩn chính sau đây.

1 Thuật toán đơn giản, dễ hiểu, dễ cài đặt (dễ viết chương trình)

2 Thuật toán sử dụng tiết kiện nhất nguồn tài nguyên của máy tính, và đặc biệt, chạy nhanh nhất có thể đƣợc

Khi ta viết một chương trình chỉ để sử dụng một số lần nhất định và chi phí phát triển (thời gian viết mã, nghiên cứu, thử nghiệm) vượt xa chi phí chạy chương trình, ta nên ưu tiên sự đơn giản, tính duy trì và nhanh chóng đưa sản phẩm vào vận hành thay vì tối ưu hóa toàn diện ngay từ đầu Việc tối ưu hóa mã nguồn quá sớm có thể làm tăng độ phức tạp và thời gian triển khai, khiến lợi ích về hiệu suất không tương xứng với chi phí Thay vào đó, hãy viết mã sạch, dễ hiểu và dùng profiling để xác định những khu vực có tác động thực tế, tập trung tối ưu ở các phần có ảnh hưởng lớn đến hiệu suất và chi phí vận hành, nhằm cân bằng giữa chi phí phát triển và chi phí chạy chương trình để dự án vẫn nhanh nhạy và dễ bảo trì trong sản xuất.

Trong thiết kế phần mềm, tối ưu hóa thời gian chạy là yếu tố quan trọng nhất, nhưng đôi khi ta phải viết các thủ tục hoặc hàm có thể tái sử dụng cho nhiều người dùng và nhiều bài toán khác nhau Khi đó chi phí thời gian thực thi có thể vượt xa chi phí viết mã ban đầu, vì vậy việc xây dựng các thủ tục hoặc thư viện dùng chung sẽ mang lại lợi ích lớn Các thuật toán như sắp xếp và tìm kiếm được áp dụng rộng rãi và được tái sử dụng ở nhiều bài toán khác nhau, nên ta dựa trên tiêu chuẩn thứ hai và tối ưu tổng thể hệ thống Trong trường hợp này ta có thể triển khai các thuật toán phức tạp miễn sao chương trình cuối cùng chạy nhanh hơn các phương án thay thế và đáp ứng nhu cầu hiệu suất của người dùng.

Tiêu chuẩn (2) đƣợc xem là tính hiệu quả của thuật toán Tính hiệu quả của thuật toán bao gồm hai nhân tố cơ bản

1 Dung lượng không gian nhớ cần thiết để lưu giữ các dữ liệu vào, các kết quả tính toán trung gian và các kết quả của thuật toán

2 Thời gian cần thiết để thực hiện thuật toán (ta gọi là thời gian chạy chương trình, thời gian này không phụ thuộc vào các yếu tố vật lý của máy tính (tốc độ xử lý của máy tính, ngôn ngữ viết chương trình ))

Trong phân tích thuật toán, chúng ta tập trung vào thời gian thực thi như thước đo chính cho độ phức tạp thuật toán, tức là đánh giá thời gian chạy để so sánh hiệu suất Khi bàn về độ phức tạp, điều này đồng nghĩa với việc xem xét thời gian thực hiện của thuật toán Một thuật toán được coi là hiệu quả khi thời gian chạy của nó ngắn hơn so với các thuật toán khác cùng một bài toán.

1.4.1.Đánh giá thời gian thực hiện của giải thuật

Có hai cách tiếp cận để đánh giá thời gian thực hiện của một thuật toán: phân tích độ phức tạp (phương pháp lý thuyết) và kiểm thử thực nghiệm (phương pháp thử nghiệm) Phương pháp phân tích cho phép ước lượng thời gian thực thi dựa trên kích thước đầu vào và các bước thực hiện của thuật toán, từ đó cho thấy xu hướng tăng theo quy mô và các giới hạn hiệu năng Phương pháp thử nghiệm nằm ở việc viết chương trình và cho chạy nó với các dữ liệu đầu vào khác nhau trên một máy tính cụ thể; thời gian chạy phụ thuộc vào các yếu tố như kích thước đầu vào, tính chất dữ liệu, cấu hình phần cứng, tối ưu hóa của trình biên dịch và quản lý bộ nhớ Kết hợp hai phương pháp sẽ mang lại bức tranh toàn diện về hiệu năng của thuật toán và giúp quyết định tối ưu hóa khi triển khai thực tế.

2 Chương trình dịch để chuyển chương trình nguồn thành chương trình mã máy

3 Tốc độ thực hiện các phép toán của máy tính đƣợc sử dụng để chạy chương trình

Thời gian chạy của một chương trình phụ thuộc vào nhiều yếu tố như cấu hình phần cứng, tải hệ thống, tối ưu hóa mã nguồn và hoạt động của hệ thống đầu vào/đầu ra, nên không thể biểu diễn chính xác thời gian chạy bằng một đơn vị thời gian chuẩn cố định Trong thực tế, thời gian thực thi thường được ước lượng hoặc mô tả ở dạng phạm vi với các đơn vị đo như giây hoặc mili-giây tùy ngữ cảnh, và sẽ dao động tùy thuộc vào mức tối ưu hóa và điều kiện vận hành hệ thống.

Trong phân tích lý thuyết, thời gian thực thi của thuật toán được xem như một hàm số của kích thước dữ liệu đầu vào Kích thước này là tham số đặc trưng cho dữ liệu và ảnh hưởng quyết định đến thời gian chạy của chương trình Việc lựa chọn kích thước dữ liệu đầu vào phụ thuộc vào từng thuật toán cụ thể Ví dụ, với các thuật toán sắp xếp mảng, kích thước dữ liệu đầu vào là số phần tử của mảng; với bài toán giải hệ n phương trình tuyến tính với n ẩn, kích thước dữ liệu đầu vào là n Thông thường dữ liệu đầu vào được biểu diễn bằng một số nguyên dương n, và ta dùng hàm T(n) để biểu diễn thời gian thực thi của thuật toán.

Trong phân tích độ phức tạp thuật toán, ta xác định thời gian thực hiện T(n) là số phép toán cơ bản cần thực hiện khi chạy thuật toán Các phép toán cơ bản được xem là các thao tác có thời gian thực hiện giới hạn bởi một hằng số, phụ thuộc vào cách triển khai cụ thể Ví dụ điển hình về các phép toán này gồm các phép toán số học như +, -, *, / và các phép toán so sánh; chúng là các yếu tố quyết định độ phức tạp thời gian của thuật toán khi kích thước đầu vào n tăng lên.

=, là các phép toán sơ cấp

1.4.2 Độ phức tạp tính toán của giải thuật

Trong đánh giá thời gian thực hiện bằng phương pháp toán học, chúng ta bỏ qua những yếu tố phụ thuộc vào cách cài đặt và chỉ tập trung vào xác định độ lớn của thời gian thực hiện T(n) Ký hiệu toán học O, đọc là ô lớn, được dùng để mô tả sự tăng trưởng của hàm T(n) theo kích thước bài toán Việc sử dụng O-notation cho phép so sánh hiệu suất giữa các thuật toán dựa trên tốc độ tăng của thời gian thực hiện khi n tăng lên, bất kể chi tiết triển khai cụ thể Do đó, phân tích độ phức tạp thời gian bằng ký hiệu O giúp nắm bắt một cách khách quan mức độ ảnh hưởng của quy mô bài toán lên thời gian thực hiện và tối ưu hóa hiệu suất.

Giả sử n là số nguyên không âm, T(n) và f(n) là các hàm thực không âm

Ta viết T(n) = O(f(n)) (đọc : T(n) là ô lớn của f(n)), nếu và chỉ nếu tồn tại các hằng số dương c và n0 sao cho T(n)  c.f(n), với  n > n0

Nếu một thuật toán có thời gian thực hiện T(n) = O(f(n)), chúng ta sẽ nói rằng thuật toán có thời gian thực hiện cấp f(n)

Vậy T(n) = O(n 2 ) Trong trường hợp này ta nói thuật toán có độ phức tạp (có thời gian thực hiện) cấp n 2

Bảng sau đây cho ta các cấp thời gian thực hiện thuật toán đƣợc sử dụng rộng rãi nhất và tên gọi thông thường của chúng

Ký hiệu ô lớn Tên gọi thông thường

Danh sách được sắp xếp theo thứ tự tăng dần của cấp thời gian thực hiện, phân biệt rõ giữa các hàm đa thức và các hàm loại mũ Các hàm đa thức như log^2 n, n, n log^2 n, n^2, n^3 cho thời gian thực hiện khả thi và các thuật toán có cấp hàm đa thức thường được xem là phù hợp với nhiều bài toán thực tế Ngược lại, các hàm loại mũ như 2^n và n! biểu thị thời gian chạy tăng nhanh theo cấp số, khiến thuật toán có thời gian thực hiện rất chậm; vì vậy cần tối ưu hoặc lựa chọn phương pháp khác để tránh các bài toán có độ phức tạp exponential.

2 Các kiểu dữ liệu cơ bản

Mục tiêu: Ghi nhớ được các kiểu dữ liệu cơ bản

Kiểu dữ liệu là một tập hợp các giá trị và một tập hợp các phép toán trên các giá trị đó

Integer is a data type that represents whole numbers in the range -32768 to 32767 and supports arithmetic operators such as +, -, *, and /, as well as integer-specific operations like div and mod Boolean is a two-valued data type with the values True and False, used in logical expressions and operated on by and, or, and not.

Kiểu dữ liệu sơ cấp là kiểu dữ liệu mà giá trị của nó là đơn nhất

Trong một hệ kiểu của ngôn ngữ lập trình, thường tồn tại một số kiểu dữ liệu được gọi là kiểu dữ liệu sơ cấp hay kiểu dữ liệu phân tử (atomic) Đây là những kiểu dữ liệu cơ bản, không thể chia nhỏ hơn trong quá trình tính toán, đóng vai trò làm nền tảng cho các thao tác xử lý dữ liệu và quá trình ép kiểu Việc nhận diện và phân loại các kiểu dữ liệu sơ cấp giúp xác định cách lưu trữ, xử lý và tối ưu hóa hiệu suất của hệ thống kiểu của ngôn ngữ.

Thông thường, các kiểu dữ liệu cơ bản bao gồm :

Kiể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

Tuỳ thuộc vào ngôn ngữ lập trình, các kiểu dữ liệu định nghĩa sẵn có thể khác nhau đôi chút, nhưng nhìn chung chúng gồm các nhóm cơ bản như số nguyên, số thực và ký tự; ví dụ với ngôn ngữ C, các kiểu dữ liệu này được xem là số nguyên, số thực và ký tự, và theo quan điểm của C, ký tự thực chất cũng là một kiểu số nguyên về mặt lưu trữ, chỉ khác ở cách sử dụng; thêm vào đó, giá trị logic đúng (TRUE) và sai (FALSE) trong C được biểu diễn bằng các giá trị nguyên khác 0 và bằng 0 tương ứng; ngược lại, Pascal định nghĩa và phân biệt rõ ràng tất cả các kiểu dữ liệu đã liệt kê ở trên, cho thấy việc phân biệt chúng một cách chặt chẽ hơn.

Sau đây là hệ kiểu của Pascal:

String là kiểu dữ liệu chứa các giá trị là nhóm ký tự hoặc chỉ một ký tự, kể cả chuỗi rỗng Độ dài tối đa của một biến String là 255 ký tự, tức là nó có thể chứa tối đa một dãy gồm 255 ký tự Đây là kiểu dữ liệu phổ biến dùng để lưu trữ dữ liệu văn bản trong nhiều ngôn ngữ lập trình và ứng dụng.

Kiểu dữ liệu String trong pascal đƣợc khai báo nhƣ sau:

Var Biến 1 , Biến 2 ,… Biếnn : String[số ký tự tối đa]

3 Kiểu bản ghi, kiểu con trỏ

Mục tiêu: Ghi nhớ được các kiểu dữ liệu bản ghi và kiểu dữ liệu con trỏ

Kiểu bản ghi

Bản ghi là một cấu trúc dữ liệu gồm nhiều phần tử có kiểu dữ liệu khác nhau nhưng có liên quan với nhau Những phần tử này được gọi là trường, và trong một bản ghi có thể có những trường mà chính chúng lại là một bản ghi khác, tạo thành một cấu trúc lồng nhau để lưu trữ thông tin một cách có tổ chức.

Kiểu dữ liệu bản ghi trong pascal đƣợc khai báo nhƣ sau:

Ví dụ: Khai báo kiểu dữ liệu bảng điểm gồm một số trường nhằm phục vụ quản lý điểm nhƣ sau:

Kiểu con trỏ

Khi khai báo một biến mặc nhiên ta qui định độ lớn vùng nhớ dành cho biến

Var x : real; y : array[1 50] of integer; nhƣ vậy biến a cần 6 byte, biến b cần 100 byte

Việc khai báo như trên thường chỉ là phỏng đoán dung lượng bộ nhớ cần thiết, chưa phải giá trị chính xác Để tránh sai sót, ta thường khai báo dư, dẫn tới lãng phí bộ nhớ Địa chỉ lưu trữ biến và việc cấp phát bộ nhớ được xác định khi biên dịch, tức là các địa chỉ và dung lượng cần cấp phát đã được cố định trước khi thực hiện các thao tác khác; đại lượng này được xem là tĩnh và không đổi suốt quá trình thực thi chương trình Để tiết kiệm bộ nhớ, khi chương trình đang chạy ta có thể cấp phát động cho các biến, được thực hiện thông qua con trỏ Muốn có biến con trỏ, ta phải định nghĩa kiểu con trỏ trước.

Kiểu con trỏ là một kiểu dữ liệu đặc biệt dùng để biểu diễn các địa chỉ

Kiểu con trỏ trong Pascal đƣợc khai báo nhƣ sau:

Tên kiểu con trỏ = ^Kiểu dữ liệu;

Bài tập thực hành của học viên

1.1.Nêu một vài cấu trúc dữ liệu cơ bản của một ngôn ngữ lập trình mà em biết

1.2 Khai báo kiểu dữ liệu Nhân sự gồm một số trường: Mã nhân sự, họ tên, lương, địa chi, nhằm phục vụ quản lý nhân sự của một cơ quan

4 Các kiểu dữ liệu trừu tượng

Mục tiêu: Ghi nhớ được khái niệm kiểu dữ liệu trừu tượng

Kiểu dữ liệu trừu tượng là một mô hình toán học đi kèm với tập hợp các phép toán trừu tượng được định nghĩa trên mô hình đó Có thể nói, kiểu dữ liệu trừu tượng là một kiểu dữ liệu do chúng ta định nghĩa ở mức khái niệm, chưa được cài đặt sẵn trong ngôn ngữ lập trình.

Khi cài đặt một kiểu dữ liệu trừu tƣợng trên một ngôn ngữ lập trình ta thực hiện hai nhiệm vụ:

 Biểu diễn kiểu dữ liệu trừu tƣợng bằng một cấu trúc dữ liệu hoặc bằng một kiểu dữ liệu trừu tƣợng khác đã đƣợc cài đặt

 Viết chương trình con thực hiện các phép toán trên kiểu dữ liệu trừu tƣợng

Một số kiểu dữ liệu trừu tƣợng: Danh sách, cây, đồ thị,

5 Các cấu trúc lưu trữ

Mục tiêu: Ghi nhớ được các cấu trúc lưu trữ cơ bản: lưu trữ kế tiếp và lưu trữ móc nối

Mảng

Mảng là một tập hợp có thứ tự, gồm n phần tử, được gọi là độ dài hay kích thước của mảng Mỗi phần tử của mảng được gắn một chỉ số để thể hiện vị trí của nó trong mảng, cho phép truy cập theo thứ tự và vị trí Các giá trị của các phần tử mảng đều cùng một kiểu dữ liệu.

Vectơ là mảng một chiều, mỗi phần tử của nó ứng với một chỉ số

Ví dụ: phần tử của vectơ A, kí hiệu là Ai hoặc A[i] với i là chỉ số

Ma trận là mảng hai chiều, mỗi phần tử của nó ứng với 2 chỉ số

Ví dụ : phần tử của ma trận B, kí hiệu Bij hoặc B[i,j] với i gọi là chỉ số hàng, j gọi là chỉ số cột

Tương tự người ta cũng mở rộng : mảng ba chiều, mảng bốn chiều,… Mảng n chiều

5.1.2 Cấu trúc lưu trữ của mảng

Một cách đơn giản, có thể hình dung bộ nhớ của máy tính điện tử (MTĐT) là một dãy các phần tử nhớ cơ sở đƣợc đánh số kế tiếp nhau ( kể từ số 0) Số thứ tự đó đƣợc gọi là địa chỉ, môt phần tử nhớ cơ sở, có địa chỉ đƣợc gọi là một từ máy Một phần tử dữ liệu có thể được lưu trữ trong máy bởi một ô nhớ bao gồm một hoặc nhiều từ máy Việc truy cập vào ô nhớ đó sẽ đƣợc xác định bởi địa chỉ của từ máy đầu tiên tạo nên ô nhớ đó Thường có hai cách để xác định được địa chỉ

Cách thứ nhất là dựa vào những đặc tả của việc lưu trữ dữ liệu để tính trực tiếp ra địa chỉ Địa chỉ loại này gọi là địa chỉ được tính Cách này thường được hay sử dụng trong chương trình dịch của các ngôn ngữ lập trình để tính địa chỉ các phần tử của mảng, tính địa chỉ các lệnh thực hiện tiếp theo v.v …

Phương pháp thứ hai là lưu trữ các địa chỉ cần thiết ở một vị trí quy định để khi cần xác định sẽ lấy từ đó ra Địa chỉ này được gọi là con trỏ (pointer) hoặc mối nối (link) Địa chỉ quay lui của chương trình con nhằm trở về vị trí lời gọi trong chương trình chính được lưu trữ ở đây; khi chương trình con kết thúc, nó sẽ trả về đúng vị trí nhờ địa chỉ quay lui này.

Cũng có một số cấu trúc lưu trữ sử dụng phối hợp cả hai cách xác định địa chỉ nói trên

Lưu trữ kế tiếp đối với mảng:

Thông thường mảng được lưu trữ trong máy dưới dạng môt vecter lưu trữ Đó là một dãy các từ máy kế tiếp nhau

Giả sử ta xét việc lưu trữ liên tiếp đối với mảng một chiều hay vectơ A, trong đó các phần tử là A[i] với 1 ≤ i ≤ n Mỗi phần tử được lưu trong một ô nhớ gồm một từ máy, nên để lưu trữ toàn bộ vectơ A ta phải dành ra n ô nhớ liên tiếp trong bộ nhớ, tức là n phần tử của vectơ được lưu trữ liên tục.

Để lưu trữ mỗi phần tử của A[i], mỗi ô nhớ của vectơ V phải chứa ω từ máy Vì vậy, để lưu trữ n phần tử liên tiếp của A, V được bố trí trên n·ω từ máy kế tiếp Địa chỉ của mỗi ô nhớ, tức là mỗi phần tử V[i], hiện là địa chỉ của từ máy đầu tiên của ô nhớ đó Ví dụ: với ω = 3 và địa chỉ của V[1] là 1000, địa chỉ của V[2] sẽ là 1003 và địa chỉ của V[3] là 1006.

Hình 2.1 Địa chỉ của V[1] đƣợc gọi là địa chỉ gốc (base address ), kí hiệu là L 0

Nhƣ vậy việc xác định địa chỉ của V[i], hay nói một cách khác : việc xác định địa chỉ của A[i] sẽ đƣợc tính ra theo công thức sau :

Trong ngôn ngữ như PASCAL, cận dưới của chỉ số không nhất thiết phải là

Địa chỉ của phần tử A[i] có thể được tính bằng LOC(A[i]) = Lo + ω × (i − b) với một số nguyên b nào đó Công thức này cho phép xác định vị trí lưu trữ của A[i] dựa trên chỉ số i và các tham số Lo, ω Đối với mảng hai chiều hay ma trận, việc lưu trữ các phần tử cũng được thực hiện bởi một vectơ lưu trữ như trên, nhằm tối ưu hóa quá trình tra cứu và sắp xếp dữ liệu trong bộ nhớ.

Đặt B là một ma trận có m hàng và n cột B được lưu trữ trong bộ nhớ dưới dạng một vectơ lưu trữ V, với V có m × n phần tử, mỗi phần tử chứa ω từ máy; tổng dung lượng nhớ để lưu trữ B là m × n × ω từ máy.

Nếu giả sử B có 3 hàng, 4 cột (m=3, n=4) thì các phần tử của nó sẽ đƣợc lưu trữ như hình sau:

Như vậy nghĩa là : các phần tử của ma trận B sẽ được lưu trữ theo hàng, hết hàng này đến hàng khác

Cách lưu trữ này được gọi là : lưu trữ theo thứ tự ưu tiên hàng

Cũng có một cách khác là lưu trữ ma trận theo thứ tự ưu tiên cột Trong phương pháp này, các phần tử của ma trận sẽ được lưu trữ theo cột, từ cột đầu tiên đến cột cuối cùng, có nghĩa là dữ liệu lần lượt điền theo từng cột trước khi sang cột kế tiếp.

Với ma trận B[3,4] như trên thì các phần tử sẽ được lưu trữ bởi vectơ lưu trữ V theo hình 2.3 :

Việc xây dựng các công thức tính địa chỉ được tiến hành tương tự như các phần trước: nếu ma trận B có m hàng, n cột và mỗi phần tử của vectơ r lưu trữ V tại vị trí tương ứng với B[i][j], thì địa chỉ của phần tử B[i][j] có thể được xác định dựa trên địa chỉ cơ sở và kích thước phần tử, tùy theo cách sắp xếp nhớ (row-major hoặc column-major) Với chuẩn row-major, địa chỉ tại (i, j) được tính addr(i, j) = base_address + ((i-1) * n + (j-1)) * size_of_element; với column-major, addr(i, j) = base_address + ((j-1) * m + (i-1)) * size_of_element Vectơ r lưu trữ V gồm các giá trị tương ứng cho từng vị trí trong ma trận B, do đó có thể liên kết mỗi V với địa chỉ của phần tử B[i][j] bằng cách ánh xạ V[k] tới địa chỉ tương ứng, giúp tối ưu truy cập bộ nhớ và đảm bảo tính nhất quán của các công thức tính địa chỉ cho mọi phần tử của B và V.

 từ máy, thì địa chỉ của B[i,j] với 1 i , j  n :

Theo thứ tự ƣu tiên hàng sẽ đƣợc tính bởi :

Theo thứ tự ƣu tiên hàng cột sẽ đƣợc tính bởi :

Trường hợp b 1  i  u 1 , b 2  j u 2 thì mỗi hàng sẽ có (u 2 – b 2 +1) phần tử Khi đó công thức tính địa chỉ, chẳng hạn : theo thứ tự ƣu tiên hàng, sẽ là :

Người ta cũng mở rộng cách lưu trữ tương tự đối với mảng nhiều chiều Chú ý:

Truy cập trực tiếp vào một phần tử của mảng thông qua địa chỉ được tính toán giúp đảm bảo tốc độ truy cập cao và đồng nhất cho mọi phần tử, bất kể vị trí của chúng trong mảng.

Trong lập trình, khi làm việc với cấu trúc mảng, chúng ta chỉ cần khai báo mảng và thao tác với tên mảng cùng các biến liên quan Các yếu tố liên quan đến cấu trúc lưu trữ của mảng và việc xác định địa chỉ truy cập tới các phần tử sẽ được trình biên dịch tự động xử lý, giúp quá trình lập trình tập trung vào xử lý dữ liệu và tối ưu hiệu suất mà không phải lo về cách bố trí bộ nhớ.

Danh sách liên kết

Trong cách tổ chức danh sách liên kết, mỗi phần tử được lưu trữ trong một ô nhớ gọi là nút (node) Mỗi nút chứa dữ liệu của phần tử và địa chỉ của nút tiếp theo (hoặc nút trước ở kiểu liên kết ngược), nhờ đó các phần tử không cần lưu trữ liền kề trong bộ nhớ và khắc phục được một số khuyết điểm của hình thức mảng Tuy nhiên, việc truy xuất một phần tử đòi hỏi duyệt qua một chuỗi nút cho đến vị trí mong muốn Có nhiều kiểu tổ chức liên kết giữa các phần tử trong danh sách, như danh sách liên kết một phía, danh sách liên kết hai phía và danh sách vòng liên kết.

Danh sách liên kết đơn: mỗi phần tử liên kết với phần tử đứng sau nó trong danh sách:

Nhƣ vậy quy cách của mỗi nút có thể hình dung nhƣ sau:

Nghĩa là mỗi nút gồm có 2 trường

Trong danh sách liên kết đơn, mỗi nút có hai trường: INFO và LINK Trường INFO chứa dữ liệu của phần tử tương ứng với nút đó, còn trường LINK là con trỏ chứa địa chỉ của nút tiếp theo Với nút cuối cùng, không có nút tiếp theo nên trường LINK chứa một địa chỉ đặc biệt được gọi là địa chỉ null hay mối nối không, dùng để đánh dấu kết thúc danh sách chứ không giống các địa chỉ ở các nút khác.

Để có thể truy cập được vào mọi nút trong danh sách liên kết, ta phải xác định được địa chỉ của nút đầu tiên Nói cách khác, ta cần nắm được con trỏ trỏ tới nút đầu danh sách Có con trỏ này trong tay, ta có thể duyệt qua từng nút kế tiếp và truy cập toàn bộ dữ liệu được lưu trữ trong danh sách một cách có hệ thống.

L, trỏ tới nút đầu tiên này

Danh sách liên kết kép: mỗi phần tử liên kết với các phần tử đứng trước và sau nó trong danh sách:

Mỗi nút trong danh sách này lại có hai trường con trỏ, theo quy cách như sau:

Ngoài trường INFO như đã đề cập trước đó, còn có trường LPTR để ghi nhận địa chỉ của nút ở bên trái (nút trước nó) và trường RPTR để ghi nhận địa chỉ của nút ở bên phải (nút sau nó).

Trong danh sách liên kết đôi, mỗi nút không chỉ biết địa chỉ của nút sau nó mà còn biết địa chỉ của nút trước nó Nút đầu tiên không có nút trước nên LPTR có giá trị null; đối với nút cuối cùng thì cũng có giá trị null.

Để duyệt danh sách hai chiều theo cả hai hướng, ta cần nắm hai con trỏ cơ bản: con trỏ L trỏ tới nút đầu tiên và con trỏ R trỏ tới nút cuối cùng Nhờ có L và R, danh sách có thể được duyệt từ đầu đến cuối hoặc ngược lại một cách thuận tiện, cho phép truy cập nhanh tới mọi phần tử và thực hiện các thao tác ở hai đầu danh sách hiệu quả Việc xác định đúng hai con trỏ này là nền tảng cho việc chèn, xóa và duyệt hai hướng trong danh sách liên kết hai chiều.

Danh sách liên kết vòng : phần tử cuối danh sách liên kết với phần tử đầu danh sách:

Bài tập thực hành của học viên

1.3.Các cấu trúc dữ liệu cơ bản của một ngôn ngữ lập trình có đủ đáp ứng mọi yêu cầu về tổ chức dữ liệu không?

1.4 Viết công thức tính địa chỉ của phần tử mảng một chiều và mảng hai chiều Cho mảng AA[15 100], BB[5 30, 7 50], biết địa chỉ gốc L = 500 và mỗi phần tử ứng với ω = 4 từ máy, mảng BB được lưu trữ theo thứ tự ưu tiên hang Tính AA[55], AA[90], BB[15,15], BB[25,40]

6.Mối quan hệ giữa CTDL và giải thuật

Mục tiêu: Ghi nhớ được mối quan hệ giữa việc xây dựng cấu trúc dữ liệu và xây dựng giải thuật cho bài toán

Thực hiện một đề án tin học là chuyển bài toán thực tế thành bài toán có thể giải quyết trên máy tính Bất kỳ bài toán thực tế nào cũng 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 đó Vì vậy, để xây dựng một mô hình tin học phản ánh đúng bài toán thực tế, cần chú trọng đến hai vấn đề cốt lõi: xác định rõ các đối tượng dữ liệu và mối quan hệ giữa chúng, cùng với xác định các thao tác xử lý, thuật toán và ràng buộc cần thực hiện trên dữ liệu Việc làm sáng tỏ hai khía cạnh này giúp thiết kế cấu trúc dữ liệu phù hợp, lựa chọn phương pháp lưu trữ tối ưu và phát triển giải pháp xử lý hiệu quả cho đề án tin học.

 Tổ chức biểu diễn các đối tƣợng thực tế :

Các thành phần dữ liệu thực tế rất đa dạng và có nhiều mối quan hệ với nhau, vì vậy trong mô hình tin học của một bài toán cần tổ chức và xây dựng các cấu trúc dữ liệu phù hợp nhất sao cho vừa phản ánh chính xác dữ liệu thực tế, vừa cho phép máy tính xử lý dễ dàng Công việc này được gọi là xây dựng cấu trúc dữ liệu cho bài toán và là bước nền tảng để tối ưu hóa lưu trữ, truy vấn và phân tích dữ liệu trong quá trình giải quyết vấn đề.

 Xây dựng các thao tác xử lý dữ liệu:

Thông qua các yêu cầu xử lý thực tế, ta xác định các thuật toán thích hợp và sắp xếp trình tự các thao tác máy tính phải thi hành để đạt được kết quả mong muốn Đây là bước thiết kế thuật toán cho bài toán, đảm bảo tính logic của giải pháp, tối ưu hóa hiệu suất xử lý và khả năng mở rộng của hệ thống.

Trong giải quyết bài toán trên máy tính, chúng ta thường tập trung vào việc xây dựng giải thuật mà quên đi tầm quan trọng của việc tổ chức dữ liệu trong bài toán; giải thuật thể hiện các thao tác xử lý và đối tượng xử lý của nó chính là dữ liệu chứa đựng các thông tin cần thiết để thực hiện các thao tác đó Để xác định giải thuật phù hợp, ta cần biết nó tác động đến loại dữ liệu nào và khi chọn cấu trúc dữ liệu cũng phải hiểu rõ những thao tác nào sẽ tác động đến nó; ví dụ, để biểu diễn đ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 thực hiện thao tác tính trung bình từ những điểm số đó Như vậy, trong một đề án tin học, giải thuật và cấu trúc dữ liệu có mối quan hệ chặt chẽ với nhau và mối quan hệ này được thể hiện qua các quyết định về cách lưu trữ và xử lý dữ liệu.

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ó các giải thuật phù hợp để tối ưu hóa xử lý và làm cho mã nguồn dễ hiểu hơn Khi cấu trúc dữ liệu thay đổi, các giải thuật cũng cần được điều chỉnh để tránh xử lý gượng ép trên một cấu trúc không phù hợp và để duy trì hiệu suất Một cấu trúc dữ liệu tốt sẽ giúp giải thuật hoạt động hiệu quả hơn, tối ưu hóa thời gian và không gian, đồng thời đơn giản hóa logic và tăng khả năng mở rộng của chương trình Nội dung này nhấn mạnh mối quan hệ giữa cấu trúc dữ liệu và giải thuật trong phát triển phần mềm, nhằm đạt được chương trình tối ưu cả về hiệu suất lẫn tính dễ bảo trì.

Ví dụ 1 minh họa một chương trình quản lý điểm thi của sinh viên cần lưu trữ các điểm số của 3 sinh viên Mỗi sinh viên có 4 điểm số tương ứng với 4 môn học khác nhau, do đó dữ liệu được lưu trữ dưới dạng bảng có 3 hàng và 4 cột, trong đó mỗi hàng đại diện cho một sinh viên và mỗi cột đại diện cho một môn học Dữ liệu có dạng như sau:

Sinh viên Môn 1 Môn 2 Môn 3 Môn 4

Chỉ xét thao tác xƣ lý là xuất điểm số các môn của từng sinhviên

Giả sử có các phương án tổ chức lưu trữ như sau:

Phương án 1: Sử dụng mảng một chiều có tất cả 3(SV) * 4(Môn) = 12 điểm số cần lưu trữ, do đó ta khai báo mảng nhƣ sau:

Type mang = array[1 12] of integer; var a: mang

Khi đó mảng a các phần tử sẽ được lưu trữ như sau:

Truy xuất điểm số môn j của sinh viên i là phần tử tại dòng i, cột j trong bảng điểm Để truy cập phần tử này, ta phải sử dụng công thức xác định chỉ số tương ứng trong mảng a, từ đó xác định vị trí và giá trị điểm số cần lấy.

Bảng điểm (dòng i, cột j)  a[ (i -1)*số cột + j ]

Đối với bất kỳ phần tử nào trong mảng, để biết đó là điểm số của sinh viên nào và môn học nào, ta dùng công thức xác định sau: a[i] được ánh xạ đến bảng điểm ở hàng (i div số cột) + 1 và cột (i mod số cột) Số cột của bảng điểm là tham số cung cấp, cho phép chuyển đổi vị trí của phần tử i trong mảng thành vị trí tương ứng trên bảng điểm, từ đó xác định được sinh viên và môn học liên quan.

ĐỆ QUY VÀ GIẢI THUẬT ĐỆ QUY

Giải thuật đệ qui

Trong giải thuật, lời giải đệ quy được xác định khi bài toán T được giải bằng lời giải của bài toán T1 có ý tưởng và nội dung giống T, nhưng tham số của T1 nhỏ hơn về kích thước Đây là cách giải thuật tái sử dụng lời giải cho một bài toán con tương tự với kích thước nhỏ hơn để xây dựng lời giải cho bài toán gốc, cho đến khi đạt được điều kiện dừng.

Chương trình đệ qui

Một chương trình con ( hàm hoặc thủ tục) được gọi là đệ qui nếu trong quá trình thực hiện nó có phần phải gọi tới chính nó

Trong chương trình con đệ qui có hai phần:

Phần neo (phần dừng) mô tả một hoặc nhiều tham số với nhiệm vụ cụ thể cần thực hiện, giúp xác định rõ phạm vi và điều kiện xử lý tại mỗi bước Phần đệ qui (qui nạp) cho thấy cách nhiệm vụ hiện tại của tham số được xác định bằng cách tham chiếu tới nhiệm vụ tương ứng với các giá trị khác, từ đó hình thành cơ chế quy nạp hoặc tái sử dụng các công việc đã xử lý để giải quyết bài toán một cách hiệu quả.

3 Các bài toán đệ quy căn bản

Mục tiêu: Thực hành (lập trình và biên dịch) với các bài toán đệ quy đơn giản.

Bài toán tính n giai thừa

Hàm này đƣợc định nghĩa nhƣ sau:

Giải thuật đệ quy được viết dưới dạng hàm dưới đây:

Begin if n=0 then Factorial:=1 else Factorial := n*Factorial(n-1);

Trong hàm trên lời gọi đến nó nằm ở câu lệnh gán sau else

Đệ quy được dùng để tính giai thừa (factorial) bằng cách gọi factorial(n) với n giảm dần cho tới khi gặp điều kiện dừng Mỗi lần gọi, n được truyền xuống một đơn vị cho tới khi n bằng 0; ví dụ, Factorial(4) gọi Factorial(3), Factorial(2), Factorial(1) và cuối cùng Factorial(0) Factorial(0) là trường hợp cơ sở và được định nghĩa là 1 để kết nối các lời gọi đệ quy lại với nhau Nhờ đó, kết quả của Factorial(4) được tính là 4 × 3 × 2 × 1 = 24.

Bài toán dãy số FIBONACCI

Dãy số Fibonacci bắt nguồn từ bài toán cổ về việc sinh sản của các cặp thỏ Bài toán đƣợc đặt ra nhƣ sau:

- Các con thỏ không bao giờ chết

- Hai tháng sau khi ra đời một cặp thỏ mới sẽ sinh ra một cặp thỏ con

- Khi đã sinh con rồi thì cứ mỗi tháng tiếp theo chúng lại sinh đƣợc một cặp con mới

Giả sử bắt đầu từ một cặp thỏ mới ra đời thì đến tháng thứ n sẽ có bao nhiêu cặp?

Ví dụ với n = 6, ta thấy

Tháng thứ 1: 1 cặp (cặp ban đầu)

Tháng thứ 2: 1 cặp (cặp ban đầu vẵn chƣa đẻ)

Tháng thứ 3: 2 cặp (đã có thêm 1 cặp con)

Tháng thứ 4: 3 cặp (cặp đầu vẫn đẻ thêm)

Tháng thứ 5: 5 cặp (cặp con bắt đầu đẻ)

Tháng thứ 6 có 8 cặp thỏ (cặp con vẫn đẻ tiếp) Đặt F(n) là số cặp thỏ ở tháng thứ n Ta nhận thấy chỉ những cặp thỏ đã có ở tháng thứ n−2 mới sinh con ở tháng thứ n, nên số cặp thỏ ở tháng thứ n bằng tổng của hai tháng trước: F(n) = F(n−1) + F(n−2) Vì vậy F(n) có thể được tính theo công thức của chuỗi Fibonacci.

Dãy số thể hiện F(n) ứng với các giá trị của n = 1, 2, 3, 4 , có dạng

1 1 2 3 5 8 13 21 34 55 nó đƣợc gọi là dãy số Fibonacci Nó là mô hình của rất nhiều hiện tƣợng tự nhiên và cũng đƣợc sử dụng nhiều trong tin học

Sau đây là thủ tục đệ quy thể hiện giải thuật tính F(n)

End; ở đây trường hợp suy biến ứng với 2 giá trị F(1) = 1 và F(2) = 1

Bài tập thực hành của học viên

2.1 Giả sử a và b là những số nguyên dương Q là hàm số của a,b, được định nghĩa nhƣ sau:

2.2 Cho biết số Fibonacci F 11 = 89 và F 12 = 144 a Hãy tính F 1 6 b Viết một thủ tục không đệ quy ( dùng phép lặp) để tính và in ra n số Fibonacci đầu tiên

2.3 Giải thuật tính ƣớc số chung lớn nhất của 2 số p và q (p > q) đƣợc mô ta nhƣ sau ( giải thuật Euclide)

Gọi r là số dƣ trong phép chia p cho q :

- Nếu r = 0 thi q là ƣớc số chung lớn nhất

Để tính ước số chung lớn nhất (USCLN) của hai số bằng thuật toán Euclid, ta lặp lại phép chia có phần dư: nếu r ≠ 0 thì gán p = q, q = r và tiếp tục cho tới khi dư bằng 0 Ví dụ với hai số 1260 và 198, bảng ghi nhận các giá trị p, q, r trong quá trình tính cho thấy r đầu tiên bằng 1260 mod 198 = 72, tiếp theo 198 mod 72 = 54, rồi 72 mod 54 = 18 và 54 mod 18 = 0; khi r = 0, USCLN là giá trị của q trước đó, ở đây là 18 Tính đệ quy của phương pháp này thể hiện ở công thức gcd(p, q) = gcd(q, p mod q) với điều kiện q ≠ 0 và gcd(p, 0) = p; từ đó có thể xây dựng một hàm đệ quy tính USCLN như gcd(p, q) = gcd(q, p % q) khi q ≠ 0, ngược lại trả về p Cách tiếp cận này cho phép triển khai một hàm tính ước số chung lớn nhất bằng đệ quy, ứng dụng dễ dàng cho bài toán ví dụ 1260 và 198 và cho phép tối ưu hóa thực thi trong nhiều ngôn ngữ lập trình.

USCLN ( p,q) c Viết một giải thuật đệ quy và một giải thuật không đệ quy (dùng phép lặp) để tính ƣớc số chung lớn nhất của p,q

{ở đây F là một vecto có n phần tử để lưu trữ n số Fibonacci đầu tiên} B1: {Tạo lập 2 số Fibonacci đầu tiên}

B2: {Tính lần lƣợt các số Fibonacci tiếp theo}

B3: {in lần lƣợt n số Fibonacci}

2.3 a Các giá trị của p,q,r đƣợc ghi nhận trong bảng sau: p q r

Ta tìm USCLN (ước chung lớn nhất) của hai số p và q bằng giải thuật Euclid Ví dụ điển hình cho USCLN là USCLN(1260, 198) = 18 Khi r = p mod q khác 0, USCLN(p, q) bằng USCLN(q, r), vì r nhỏ hơn cả p và q, nên quá trình tính toán có thể tiếp tục ở bước kế tiếp với các cặp (q, r) Đây là cơ sở của giải thuật đệ quy cho USCLN: nếu r = 0 thì kết quả là q; ngược lại, ta lặp lại với cặp (q, r) cho lần tính tiếp theo.

{Chú ý là số dƣ r của p và q đƣợc xác định bởi phép tính p mod q}

{x, y, z ở đây là các biến cục bộ}

DANH SÁCH

Khái niệm danh dách

Có thể nói, trong công việc hàng ngày danh sách là một công cụ rất phổ biến và hữu ích Những danh sách điển hình gồm danh sách các lớp nghề quản trị mạng và danh sách các sinh viên tham gia văn nghệ, cùng nhiều loại danh sách khác giúp sắp xếp công việc, theo dõi tiến độ và quản lý nguồn lực một cách dễ dàng Việc xây dựng và cập nhật danh sách một cách có hệ thống không chỉ nâng cao tính tổ chức mà còn tối ưu hóa quy trình làm việc, hỗ trợ lên kế hoạch, phối hợp và báo cáo kết quả Để bài viết dễ được tìm thấy và hiểu rõ, nên tập trung nêu rõ các danh mục và từ khóa liên quan như danh sách, quản trị mạng, lớp nghề và sinh viên tham gia văn nghệ.

Trong mọi danh sách, có một đặc điểm chung rõ ràng: danh sách chứa một số hữu hạn phần tử được sắp xếp theo thứ tự và số lượng phần tử có thể biến động tùy theo ngữ cảnh sử dụng Những đặc tính này giúp danh sách vừa duy trì trật tự vừa linh hoạt về kích thước, phù hợp cho các thao tác thêm, xóa, sắp xếp và duyệt dữ liệu.

Có thể hình dung : danh sách A là một dãy các phần tử : (a 1 ,a2, ,an) với n là một biến

Vectơ chính là hình ảnh của một danh sách tại một thời điểm nào đó

Một danh sách luôn có phần tử đầu tiên và phần tử cuối cùng, tương ứng với phần tử thứ nhất và phần tử thứ n Với mỗi phần tử, có phần tử trước nó (trừ phần tử đầu) và có phần tử sau nó (trừ phần tử cuối).

Các phép toán trên danh dách

Đối với danh sách, các thao tác cơ bản thường gồm khởi tạo danh sách rỗng, kiểm tra danh sách có rỗng hay không, chèn phần tử vào danh sách và xóa phần tử khỏi danh sách Ngoài ra còn có các phép khác như tra cứu, duyệt và cập nhật phần tử, cũng như lấy kích thước danh sách để hỗ trợ xử lý dữ liệu hiệu quả trong các ứng dụng.

Tìm kiếm một phần tử theo một tiêu chí xác định

Sắp xếp các phần tử theo một thứ tự ấn định

Ghép hai hoặc nhiều danh sách thành một danh sách lớn

Tách một danh sách thành nhiều danh sách con v.v…

2 Cài đặt danh sách theo cấu trúc mảng

- Tổ chức cài đặt cho danh sách theo cấu trúc mảng và các phép toán tương ứng với cấu trúc dữ liệu

- Giải được các bài toán sử dụng danh sách được cài đặt trên mảng

Chúng ta có thể cài đặt danh sách bằng mảng để lưu giữ liên tiếp các phần tử từ vị trí đầu tiên của mảng Với cách làm này, ta phải ước lượng số phần tử của danh sách để khai báo kích thước phù hợp cho mảng Như vậy, số lượng phần tử của mảng phải lớn hơn hoặc bằng số phần tử của danh sách, nên mảng thường còn dư một số vị trí trống Bên cạnh đó, ta cần lưu giữ độ dài hiện tại của danh sách, tức là số phần tử thực tế và cho biết phần nào của mảng còn trống.

Chúng ta mô tả danh sách đƣợc cài đặt bằng mảng nhƣ sau:

… Phần tử cuối cùng trong danh sách

Ta xem vị trí của một phần tử trong danh sách là chỉ số của mảng tại vị trí lưu trữ phần tử đó

Các khai báo cần thiết là:

{Số nguyên thích hợp để chỉ độ dài của danh sách}

Kieuphantu; {kiểu của phần tử trong danh sách}

Position = integer; { Kiểu vị trí của các phần tử }

Tenkieumang = ARRAY [Chỉ số] OF Kieuphantu;

{mảng chứa các phần tử của danh sách }

Position Last; {giữ độ dài danh sách } end;

Trong bài viết này, chúng ta biểu diễn danh sách như một kiểu dữ liệu trừu tượng bằng cấu trúc dữ liệu mảng, giúp hiểu rõ cách lưu trữ và thao tác với danh sách một cách có tổ chức Việc sử dụng mảng để biểu diễn danh sách cung cấp nền tảng cho các khái niệm về kiểu dữ liệu trừu tượng và các phép toán trên danh sách một cách trực quan và hiệu quả Phần tiếp theo sẽ cài đặt các phép toán cơ bản trên danh sách, bao gồm chèn, xóa, tìm kiếm và duyệt phần tử, nhằm minh họa cách triển khai danh sách trong thực tế và tối ưu hóa hiệu suất xử lý.

Khởi tạo danh sách rỗng

Danh sách rỗng là danh sách không chứa bất kỳ phần tử nào (hay độ dài danh sách bằng 0) Theo cách khai báo trên, trường Last là vị trí của phần tử cuối cùng trong danh sách và cũng là độ 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 Last bằng 0 Procedure MakeNull_List;

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

Chèn phần tử vào danh sách

Khi chèn phần tử có nội dung x vào tại vị trí p của danh sách L thì sẽ xuất hiện các khả năng sau:

Mảng đầy là trạng thái khi mọi phần tử của mảng đã chứa các phần tử của danh sách, nghĩa là phần tử cuối cùng của danh sách nằm ở vị trí cuối cùng của 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 chèn không thể thực hiện được và chương trình con gặp lỗi.

- Ngƣợc lại ta tiếp tục xét:

Kiểm tra tính hợp lệ của vị trí p trước khi chèn vào danh sách đặc là cần thiết Nếu p không hợp lệ (p > last + 1 hoặc p < 1) thì chương trình con sẽ gặp lỗi Cụ thể, p < 1 cho thấy p không phải là một vị trí hợp lệ trong danh sách đặc, trong khi p > last + 1 sẽ làm danh sách đặc mất tính đặc thù vì còn một vị trí trong mảng chưa có nội dung.

Nếu vị trí p hợp lệ thì chúng ta tiến hành xen theo các bước sau:

+ Dời các phần tử từ vị trí p đến cuối danh sách ra sau 1 vị trí

+ Độ dài danh sách tăng 1

+ Đƣa phần tử mới vào vị trí p

Chương trình con chèn phần tử x vào vị trí p của danh sách L có thể viết nhƣ sau:

Procedure Insert_List(X : Kieuphantu, P : Position);

Begin if (Last=MaxLength) then

Write("Danh sach day"); else if ((P < 1) or (P > Last))

Write("Vi tri khong hop le");

{Dời các phần tử từ vị trí p đến cuối danh sách sang phải 1 vị trí}

{Tăng độ dài danh sách lên 1 } Last:=Last + 1;

Xóa phần tử khỏi danh sách

Xoá một phần tử ở vị trí p ra khỏi danh sách L chúng ta làm công việc ngƣợc lại với xen

Đầu tiên, kiểm tra vị trí của phần tử cần xóa để đảm bảo nó hợp lệ trong danh sách Nếu p > L.length hoặc p < 1 thì đây không phải là vị trí của phần tử trong danh sách, và thao tác xóa cần được xử lý tương ứng.

Ngược lại, với vị trí p hợp lệ trong danh sách, các phần tử từ vị trí p+1 đến cuối danh sách được dịch sang trái một vị trí, khiến danh sách ngắn lại đi 1 phần tử.

Begin if ((PLast)) then

Write ("Vi tri khong hop le"); else if (Last

Ngày đăng: 31/07/2022, 11:09

Nguồn tham khảo

Tài liệu tham khảo Loại Chi tiết
7. Trần Hạnh Nhi, Giáo trình cấu trúc dữ liệu, Trường đại học Khoa hoc tựnhiên, tp. Hồ Chí Minh, 2003 Sách, tạp chí
Tiêu đề: Giáo trình cấu trúc dữ liệu
Tác giả: Trần Hạnh Nhi
Nhà XB: Trường đại học Khoa học Tự nhiên
Năm: 2003
8. PGS. TS. Hoàng Nghĩa Tý, Cấu Trúc Dữ Liệu Và Thuật Toán, Xây Dựng, 2002 Sách, tạp chí
Tiêu đề: Cấu Trúc Dữ Liệu Và Thuật Toán
9. Gia Việt(Biên dịch), ESAKOV.J , WEISS T, Bài Tập Nâng Cao Cấu Trúc Dữ Liệu Cài Đặt Bằng C, Nhà xuất bản: Thống kê Sách, tạp chí
Tiêu đề: Bài Tập Nâng Cao Cấu Trúc Dữ Liệu Cài Đặt Bằng C
Nhà XB: Nhà xuất bản: Thống kê
10. Minh Trung (Biên dịch), TS. Khuất Hữu Thanh(Biên dịch), Chu Trọng Lương(Tác giả), 455 Bài Tập Cấu Trúc Dữ Liệu - Ứng Dụng Và Cài Đặt Bằng C++, Thống kê Sách, tạp chí
Tiêu đề: 455 Bài Tập Cấu Trúc Dữ Liệu - Ứng Dụng Và Cài Đặt Bằng C++, Thống kê
Tác giả: Chu Trọng Lương, Minh Trung, Khuất Hữu Thanh
11. Robert Sedgewick, Trần Đan Thƣ(Biên dịch), Bùi thị Ngọc Nga(Biên dịch), Cẩm Nang Thuật Toán (Tập1,2); Khoa học và kỹ thuật Sách, tạp chí
Tiêu đề: Cẩm Nang Thuật Toán (Tập1,2)
12. GS. TSKH. Hoàng Kiếm, Giải Một Bài Toán Trên Máy Tính Như Thế Nào, Giáo dục, 2005 Sách, tạp chí
Tiêu đề: Giải Một Bài Toán Trên Máy Tính Như Thế Nào
1. Aho, Hopcroft &amp; Ullman, Data Structures and Algorithms,Addison Wesley, 2001 Khác
2. Robert Sedewick, Algorithms in Java Third Edition,Addison Wesley, 2002. Niklaus Wirth, Data Structures and Algorithms,Prentice Hall, 2004.Robert Sedewick, Algorithms,Addison Wesley, 1983 Khác
3. Bruno R. Preiss, Data Structures and Algorithms with Object-Oriented Design, Jon Wiley &amp; Sons, 1998 Khác
4. PGS.TS.Đỗ Xuân Lôi – Cấu trúc dữ liệu và giải thuật – NXB Khoa học và Kỹ thuật – 1997 Khác
5. Đỗ Xuân Lôi Cấu trúc dữ liệu và giải thuật NXB Đại học Quốc gia Hà Nội, 2009 Khác
6. PGS.TS.Đỗ Xuân Lôi – Giáo trình Cấu trúc dữ liệu và giải thuật – Vụ giáo dục chuyên nghiệp, NXB Giáo dục - 2002 Khác
13. Nguyễn Quốc Lƣợng, Hoàng Đức Hải – Cấu trúc dữ liệu + giải thuật = chương trình – NXB Giáo dục – 1996 Khác

TỪ KHÓA LIÊN QUAN

TÀI LIỆU CÙNG NGƯỜI DÙNG

TÀI LIỆU LIÊN QUAN

🧩 Sản phẩm bạn có thể quan tâm