1. Trang chủ
  2. » Giáo Dục - Đào Tạo

Giáo trình cấu trúc dữ liệu và giải thuật (nghề lập trình máy tính, tin ứng dụng trình độ CĐTC)

80 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

Tiêu đề Giáo trình cấu trúc dữ liệu và giải thuật (nghề lập trình máy tính, tin ứng dụng trình độ cao đẳng nghề & trung cấp nghề)
Tác giả Trần Thị Kim Ngọc
Người hướng dẫn P. T. S. Nguyễn Văn A
Trường học Trường Cao Đẳng Nghề An Giang
Chuyên ngành Hệ nghề lập trình máy tính và tin ứng dụng
Thể loại Giáo trình
Năm xuất bản 2018
Thành phố An Giang
Định dạng
Số trang 80
Dung lượng 648,57 KB

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: GIỚI THIỆU CẤU TRÚC DỮ LIỆU VÀ GIẢI THUẬT (8)
    • I. MỐI LIÊN HỆ GIẢI THUẬT VÀ CẤU TRÚC DỮ LIỆU (8)
    • II. ĐÁNH GIÁ ĐỘ PHỨC TẠP CỦA GIẢI THUẬT (10)
      • 1. Sự cần thiết phải phân tích giải thuật (10)
      • 2. Thời gian thực hiện của giải thuật (11)
      • 3. Tỷ suất tăng và độ phức tạp của giải thuật (12)
      • 4. Cách tính độ phức tạp (13)
    • III. BÀI TẬP (21)
  • CHƯƠNG 2: CÁC KIỂU DỮ LIỆU NÂNG CAO (23)
    • I. MẢNG (23)
      • 1. Mảng 1 chiều (23)
      • 2. Mảng nhiều chiều (25)
    • II. CON TRỎ (26)
      • 1. Cấp phát tĩnh, cấp phát động và con trỏ (26)
      • 2. Sự cài đặt (27)
    • III. CẤU TRÚC (27)
      • 1. Định nghĩa kiểu cấu trúc (27)
      • 2. Khai báo biến cấu trúc (29)
    • IV. BÀI TẬP (29)
  • CHƯƠNG 3: DANH SÁCH (31)
    • I. KHÁI NIỆM DANH SÁCH (31)
      • 1. Các phép toán trên danh sách (31)
      • 2. Cài đặt danh sách bằng mảng (danh sách đặc) (33)
    • II. CÀI ĐẶT DANH SÁCH BẰNG CON TRỎ (DANH SÁCH LIÊN KẾT) 36 I NGĂN XẾP (STACK) (37)
      • 1. Định nghĩa ngăn xếp (42)
      • 2. Các phép toán trên ngăn xếp (42)
      • 3. Cài đặt ngăn xếp (43)
    • IV. HÀNG ĐỢI (QUEUE) (46)
      • 1. Định Nghĩa (46)
      • 2. Các phép toán cơ bản trên hàng (46)
      • 3. Cài đặt hàng (46)
    • V. MỘT SỐ ỨNG DỤNG CỦA DANH SÁC H (54)
      • 1. Đảo ngƣợc xâu k ý tự (0)
      • 2. Tính giá trị của biểu thức dạng hậu tố (55)
      • 3. Chuyển đổi biểu thức dạng trung tố sang hậu tố (57)
    • VI. BÀI TẬP (60)
      • 1. Bài tập cơ bản (60)
      • 2. Bài tập nâng cao (61)
  • CHƯƠNG 4: SẮP XẾP VÀ TÌM KIẾM (62)
    • I. GIỚI THIỆU VỀ SẮP XẾP VÀ TÌM KIẾM (62)
    • II. CÁC PHƯƠNG PHÁP SẮP XẾP (63)
      • 1. Sắp xếp kiểu chọn (Selection Sort) (63)
      • 2. Sắp xếp kiểu chèn ( Insertion Sort) (64)
      • 3. Sắp xếp nổi bọt (Bubble Sort) (66)
      • 4. Quick sort (68)
      • 5. Heap sort (71)
    • III. CÁC PHƯƠNG PHÁP TÌM KIẾM (74)
      • 1. Tìm kiếm tuần tự (74)
      • 2. Tìm kiếm nhị phân (75)
      • 3. Tìm kiếm tam phân (76)
  • TÀI LIỆU THAM KHẢO (80)

Nội dung

MỐI LIÊN HỆ GIẢI THUẬT VÀ CẤU TRÚC DỮ LIỆU Theo quan điểm của phân tích thiết kế hướng đối tượng, mỗi lớp sẽ được xây dựng với một số chức năng nào đó và các đối tượng của nó sẽ tham gia

GIỚI THIỆU CẤU TRÚC DỮ LIỆU VÀ GIẢI THUẬT

MỐI LIÊN HỆ GIẢI THUẬT VÀ CẤU TRÚC DỮ LIỆU

Theo quan điểm của phân tích thiết kế hướng đối tượng, các lớp được xây dựng để đảm nhiệm các chức năng riêng biệt và góp phần vào hoạt động chung của chương trình, đồng thời tận dụng tính đóng kín và khả năng sử dụng lại của các class Các lớp trong phần mềm biên dịch thường gồm hàng loạt thư viện lớp đa dạng, phục vụ nhiều mục đích khác nhau như đọc/ghi dữ liệu với thiết bị ngoại vi, cung cấp các chức năng vẽ và tô màu trong đồ họa, xử lý giao tiếp người dùng qua bàn phím, chuột, màn hình, hay các lớp hỗ trợ truyền nhận dữ liệu qua mạng Các lớp cấu trúc dữ liệu cũng không ngoại lệ, và có thể phân loại thành hai nhóm chính dựa trên đặc điểm và chức năng của chúng.

- Các lớp có khả năng lưu trữ và xử lý lượng dữ liệu lớn.

Nhóm thứ hai tập trung vào các lớp cấu trúc dữ liệu, chủ đề quan trọng trong lĩnh vực lập trình và phát triển phần mềm Các lớp cấu trúc dữ liệu có điểm giống với các lớp khác ở chỗ đều là các thành phần tổ chức mã nguồn, nhưng khác ở chỗ chúng đặc biệt tập trung vào cách tổ chức và xử lý dữ liệu hiệu quả Hiểu rõ sự khác biệt này giúp tối ưu hóa hiệu suất ứng dụng và nâng cao khả năng quản lý dữ liệu trong các dự án phần mềm.

Các lớp cấu trúc dữ liệu đều có điểm chung là thực hiện các chức năng thông qua hành vi của các đối tượng bên trong, đảm bảo nhiệm vụ được giao phó sau khi xây dựng Khác với quan điểm hướng thủ tục trước đây, các lớp dữ liệu có tính đóng kín và khả năng tái sử dụng cao, nâng cao hiệu quả và dễ bảo trì Mặc dù các chương trình dựa trên lớp có thể chạy nhanh hơn về mặt thực thi, nhưng chúng thường gặp phải các hạn chế về thời gian phát triển thuật toán chậm, gây khó khăn cho lập trình viên, cũng như thiếu tính trong sáng và phương án sửa lỗi, mở rộng sản phẩm phức tạp hơn.

Các lớp cấu trúc dữ liệu có đặc trưng riêng biệt, nhiệm vụ chính là lưu trữ dữ liệu một cách hiệu quả để đáp ứng nhanh chóng khi chương trình yêu cầu truy xuất thông tin cụ thể Những thao tác cơ bản của cấu trúc dữ liệu bao gồm thêm dữ liệu mới và xóa bỏ dữ liệu đã tồn tại, giúp duy trì tính linh hoạt và tối ưu hóa hiệu suất hoạt động của hệ thống phần mềm.

8 có, tìm kiếm, truy xuất.

Khi thiết kế các giải thuật để giải các bài toán lớn, việc lựa chọn cấu trúc dữ liệu phù hợp rất quan trọng Ngoài các thao tác dữ liệu cơ bản, từng cấu trúc dữ liệu còn có các thao tác bổ sung riêng, ảnh hưởng đến hiệu suất và khả năng xử lý của thuật toán Việc hiểu rõ đặc điểm và chức năng của các cấu trúc dữ liệu khác nhau giúp tối ưu hóa quá trình xử lý dữ liệu trong các bài toán phức tạp Chọn đúng cấu trúc dữ liệu sẽ nâng cao hiệu quả giải thuật và tiết kiệm thời gian tính toán.

Chúng ta thử xem xét một ví dụ thật đơn giản sau đây:

Trong ví dụ về việc viết chương trình để nhập một dãy số và in chúng ra theo thứ tự ngược lại, việc quản lý dữ liệu thủ công bằng cách khai báo biến lưu trữ các giá trị và sắp xếp thứ tự in đã làm chúng ta bỏ lỡ nguyên tắc lập trình hướng đối tượng Thay vì lo lắng về các chi tiết nhỏ này, chúng ta nên tận dụng cấu trúc dữ liệu để tổ chức và xử lý dữ liệu một cách tối ưu hơn Nhớ rằng, việc thiết kế và lưu trữ dữ liệu không cần thiết phải quá tỉ mỉ vào thời điểm ban đầu, chính là cách để hiểu rõ vai trò của các lớp cấu trúc dữ liệu trong lập trình.

Môn cấu trúc dữ liệu và giải thuật giúp người học hiểu rõ các lớp cấu trúc dữ liệu có sẵn trong phần mềm và nắm bắt các phương pháp, nguyên tắc chung trong xây dựng các lớp này Việc học cách xây dựng các lớp cấu trúc dữ liệu từ đơn giản đến phức tạp giúp nâng cao kỹ năng và khả năng lựa chọn cấu trúc phù hợp cho từng bài toán Ngoài ra, kiến thức về cấu trúc dữ liệu còn cho phép phát triển các lớp phức tạp hơn, tinh vi hơn và phù hợp hơn với các yêu cầu cụ thể Khả năng kế thừa các cấu trúc dữ liệu có sẵn để phát triển các tính năng mở rộng cũng là một lợi thế quan trọng của môn học này.

Người đã từng tiếp xúc với lập trình đều quen thuộc với khái niệm "ngăn xếp", một cấu trúc dữ liệu đơn giản nhưng rất phổ biến trong lập trình Chúng ta sẽ có cơ hội học kỹ hơn về cấu trúc này trong quá trình học tập Mục đích của việc sử dụng ví dụ về ngăn xếp là để minh họa và giúp người đọc làm quen với phương pháp tiếp cận nhất quán xuyên suốt toàn bộ giáo trình.

Ngăn xếp là cấu trúc dữ liệu chứa đựng dữ liệu theo nguyên tắc Last In, First Out (LIFO), nghĩa là dữ liệu đưa vào sau sẽ được lấy ra trước Cấu trúc ngăn xếp giúp chương trình xử lý dữ liệu dễ dàng, đơn giản hóa quá trình lưu trữ và truy xuất dữ liệu theo quy tắc bất di bất dịch này Khi áp dụng cấu trúc dữ liệu ngăn xếp, các thao tác như đẩy và xổ dữ liệu trở nên dễ dàng và hiệu quả hơn, phù hợp với nhiều bài toán cần xử lý theo thứ tự đảo ngược Nhờ vậy, ngăn xếp là thành phần không thể thiếu trong lập trình và thiết kế phần mềm, giúp tối ưu hóa hiệu năng và độ tin cậy của hệ thống.

Lặp cho đến khi nhập đủ các con số mong muốn

Cất vào ngăn xếp con số vừa nhập.

Lặp trong khi mà ngăn xếp vẫn còn dữ liệu

Lấy từ ngăn xếp ra một con số In số vừa lấy đƣợc

Ngăn xếp là một cấu trúc dữ liệu quan trọng giúp giải quyết các bài toán phức tạp một cách dễ dàng và hiệu quả Tính đóng kín của các lớp trong ngăn xếp đảm bảo chương trình hoạt động trong sáng và rõ ràng Mặc dù đoạn mã trên không hiển thị trực tiếp cách ngăn xếp xử lý dữ liệu, nhưng chúng ta có thể hoàn toàn yên tâm về khả năng làm việc của nó khi đã được lập trình phù hợp Nhờ vào các cấu trúc dữ liệu phù hợp, các lập trình viên có thể dễ dàng xử lý các bài toán lớn, đồng thời tập trung vào xây dựng, tinh chế thuật toán và kiểm tra lỗi hiệu quả hơn.

Giải thuật đóng vai trò quan trọng trong việc xây dựng kịch bản hoạt động của các đối tượng trong lập trình hướng đối tượng, giúp xác định thứ tự và điều kiện thực hiện các tác vụ Trong thiết kế phần mềm, giải thuật liên quan đến việc lập trình thủ tục để xác định các bước cần thực hiện trong từng chức năng của lớp Đặc biệt, sau khi xác định các phương thức của lớp, chúng ta sử dụng giải thuật để xử lý dữ liệu bên trong, đảm bảo các chức năng của phương thức được thực hiện chính xác và hiệu quả Do đó, giải thuật không chỉ giúp lập trình chính xác, rõ ràng mà còn tối ưu hóa quá trình xử lý dữ liệu trong các lớp của hệ thống.

Trong môn học này, chúng ta sẽ tập trung tìm hiểu các giải thuật chủ yếu liên quan đến phương thức của các lớp Cấu trúc dữ liệu Các giải thuật này bao gồm các thuật toán sắp xếp, tìm kiếm, giúp tối ưu hoá quá trình xử lý dữ liệu Đồng thời, các ứng dụng minh họa sử dụng các lớp Cấu trúc dữ liệu để giải quyết các bài toán thực tế, lý giải cách áp dụng giải thuật trong các tình huống cụ thể.

ĐÁNH GIÁ ĐỘ PHỨC TẠP CỦA GIẢI THUẬT

1 Sự cần thiết phải phân tích giải thuật:

Trong quá trình giải quyết một bài toán, chúng ta thường có nhiều giải pháp khác nhau và cần đánh giá các giải pháp này để chọn ra phương án tối ưu nhất Đánh giá các giải thuật dựa trên các tiêu chí quan trọng như độ tối ưu, hiệu quả thực thi và độ phức tạp là bước quan trọng để đảm bảo chọn được giải pháp phù hợp Việc lựa chọn giải thuật tốt giúp tối ưu hóa kết quả, giảm thiểu thời gian và tài nguyên tiêu thụ, mang lại hiệu quả cao cho quá trình giải quyết bài toán.

(3) Giải thuật thực hiện nhanh

Để kiểm tra tính đúng đắn của giải thuật, chúng ta có thể cài đặt và thử nghiệm trên máy với các bộ dữ liệu mẫu, sau đó so sánh kết quả thu được với kết quả đã biết Tuy nhiên, phương pháp này không đảm bảo vì giải thuật có thể đúng với các bộ dữ liệu đã thử nhưng sai với bộ dữ liệu khác Cách làm này chỉ giúp phát hiện giải thuật sai chứ chưa chứng minh được tính đúng đắn của nó Tính đúng đắn của giải thuật cần phải được chứng minh bằng lý thuyết toán học, điều này không đơn giản và không được trình bày trong nội dung này.

Khi viết chương trình để sử dụng nhiều lần, yếu tố quan trọng nhất là khả năng viết thuật toán dễ dàng và nhanh chóng để đạt kết quả mong muốn Thời gian chạy của chương trình không cần tối ưu cao vì nó chỉ được sử dụng trong một số lần hạn chế Điều này giúp tiết kiệm công sức lập trình và đảm bảo hiệu quả trong việc phát triển phần mềm.

Khi một chương trình được sử dụng nhiều lần, yêu cầu tiết kiệm thời gian thực hiện trở nên vô cùng quan trọng, đặc biệt đối với các chương trình xử lý dữ liệu nhập lớn Do đó, hiệu quả thời gian thực hiện giải thuật cần được xem xét kỹ lưỡng để đảm bảo tối ưu hóa quá trình xử lý.

2 Thời gian thực hiện của giải thuật:

Một phương pháp hiệu quả để đánh giá hiệu quả của một thuật toán là lập trình và đo lường thời gian thực thi của nó trên một máy tính cụ thể, dựa trên tập hợp dữ liệu đầu vào đã được chọn lọc Phương pháp này giúp xác định chính xác hiệu suất của thuật toán trong các điều kiện thực tế, từ đó tối ưu hóa quá trình xử lý dữ liệu Việc đo lường thời gian thực hiện giúp các lập trình viên và nhà nghiên cứu hiểu rõ hơn về khả năng hoạt động của thuật toán và cải tiến nó để phù hợp với yêu cầu của các ứng dụng cụ thể.

Thời gian thực hiện không chỉ liên quan đến thuật toán mà còn chịu ảnh hưởng bởi tập lệnh của máy tính, chất lượng phần cứng và kỹ năng của người lập trình, ảnh hưởng đến hiệu suất tổng thể của chương trình.

Trong quá trình thi hành, hiệu quả của các thuật toán có thể được điều chỉnh để tối ưu hóa trên các tập dữ liệu đặc biệt đã chọn Để vượt qua các trở ngại này, các nhà khoa học máy tính đã xem xét độ phức tạp của thời gian như một chỉ số cơ bản đo lường hiệu quả thực thi của thuật toán Thuật ngữ "tính hiệu quả" thường đề cập đến khả năng tối ưu trong thời gian thực hiện, đặc biệt là trong các trường hợp xấu nhất Thời gian thực hiện chương trình đóng vai trò quan trọng trong đánh giá hiệu suất và độ hiệu quả của thuật toán, giúp tối ưu hóa quá trình xử lý dữ liệu trong lập trình và công nghệ thông tin.

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

Chương trình tính tổng của n số có thời gian thực hiện là T(n) = cn trong đó c là một hằng số

Thời gian thực hiện chương trình là một hàm không âm, tức là T(n)  0  n 

0 b Đơn vị đo thời gian thực hiện Đơn vị của T(n) không phải là đơn vị đo thời gian bình thường như giờ, phút giây … mà thường được xác định bởi số các lệnh được thực hiện trong một máy tính lý tưởng

Trong phân tích thuật toán, khi nói thời gian thực hiện của một chương trình là T(n) = cn, điều này có nghĩa là chương trình đó cần thực thi tổng cộng cn chỉ thị Thời gian thực hiện này thể hiện số lượng thao tác tối thiểu và tối đa mà chương trình cần để hoàn thành công việc Trong trường hợp xấu nhất, thời gian chạy của thuật toán vẫn dựa trên hàm T(n) = cn, giúp xác định khả năng hiệu quả của chương trình dưới tải trọng lớn nhất Đây là khái niệm quan trọng trong tối ưu hóa mã nguồn và đánh giá độ phức tạp của các thuật toán, nhằm đảm bảo hiệu suất ổn định và đáp ứng yêu cầu của các hệ thống phần mềm.

Thời gian thực hiện của một chương trình phụ thuộc không chỉ vào kích thước dữ liệu mà còn vào tính chất của dữ liệu đó Dù dữ liệu có cùng kích thước, thời gian chạy có thể khác nhau tùy thuộc vào loại dữ liệu đầu vào, ví dụ như sắp xếp một dãy số đã có thứ tự sẽ mất ít thời gian hơn so với dãy chưa được sắp xếp Ngoài ra, việc đưa dữ liệu vào các dãy đã có thứ tự tăng hoặc giảm cũng ảnh hưởng đáng kể đến thời gian thực thi của chương trình.

Trong phân tích thuật toán, ta thường xem T(n) là thời gian thực hiện chương trình trong trường hợp xấu nhất đối với dữ liệu đầu vào có kích thước n T(n) thể hiện thời gian tối đa cần thiết để thực thi chương trình với mọi dữ liệu đầu vào cùng kích thước n, giúp đánh giá hiệu suất và độ phức tạp của thuật toán một cách chính xác.

3 Tỷ suất tăng và độ phức tạp của giải thuật a Tỷ suất tăng:

Ta nói rằng hàm không âm T(n) có tỷ suất tăng (growth rate) f(n) nếu tồn tại các hằng số c và no sao cho T(n)  cf(n) với mọi n  no

Ta có thể chứng minh đƣợc rằng “ Cho một hàm không âm T(n) bất kỳ, ta luôn tìm đƣợc tỷ suất tăng f(n) của nó”

Giả sử T(0) = 1, T(1) = 4 và tổng quát T(n) = (n+1) 2 Đặt no = 1 và c = 4 thì với mọi n  1 chúng ta dễ dàng chứng minh rằng T(n) = (n+1) 2  4n 2 với mọi n  1, tức là tỷ suất tăng của T(n) là n 2

Tỷ suất tăng của hàm T(n) = 3n 3 +2n 2 là n 3 Thực vậy, cho no = 0 và c = 5 ta dễ dàng chứng minh rằng với mọi n  0 thì 3n 3 + 2n 2  5n 3

12 b Khái niệm độ phức tạp của giải thuật

Giả sử ta có hai giải thuật P1 và P2 với thời gian thực hiện tương ứng là T1(n)

Trong phân tích độ phức tạp thuật toán, ta so sánh hai hàm thời gian T1(n) = 100n² và T2(n) = 5n³ dựa trên tỷ suất tăng của chúng Khi n nhỏ hơn 20, thuật toán P2 (với thời gian T2) thực hiện nhanh hơn P1 vì hệ số của 5n³ nhỏ hơn hệ số của 100n² (5 < 100) Tuy nhiên, khi n lớn hơn 20, thuật toán P1 trở nên nhanh hơn do số mũ của 100n² thấp hơn số mũ của 5n³, điều này cho thấy khả năng mở rộng của thuật toán theo dữ liệu.

Trong bài viết này, chúng ta chỉ tập trung vào trường hợp n > 20 vì khi n nhỏ hơn 20, thời gian thực hiện của cả P1 và P2 đều không đáng kể và sự khác biệt giữa T1 và T2 cũng trở nên không rõ ràng Điều này giúp làm rõ rằng phân tích chỉ mang ý nghĩa khi n lớn hơn 20, nơi mà sự khác biệt về thời gian giữa hai phương pháp trở nên rõ ràng và đáng chú ý.

Chúng ta nên xem xét tỷ suất tăng của hàm thời gian thực hiện chương trình thay vì tập trung vào chính thời gian thực hiện Điều này giúp đánh giá hiệu quả của quá trình tối ưu hóa và nâng cao năng suất của hệ thống Phân tích tỷ suất tăng cũng cung cấp cái nhìn rõ nét hơn về xu hướng tăng giảm của thời gian thực thi, từ đó đưa ra các chiến lược cải thiện phù hợp Việc này đặc biệt hữu ích trong các giải pháp tối ưu hóa phần mềm nhằm giảm thiểu thời gian xử lý và tăng tốc độ hoạt động của chương trình.

Trong phân tích thuật toán, hàm T(n) gọi là có độ phức tạp f(n) nếu tồn tại các hằng c, No sao cho T(n)  cf(n) với mọi n  No, thể hiện rằng T(n) có tỷ suất tăng không vượt quá f(n) Ký hiệu T(n) là O(f(n)) (được đọc là "ô của f(n)") để mô tả độ phức tạp lớn tối đa của thuật toán dựa trên chức năng thời gian.

T(n) = (n+1) 2 có tỷ suẩt tăng là n 2 nên T(n) = (n+1) 2 là O(n 2 )

Chú ý: O(c.f(n)) = O(f(n)) với c là hằng số Đặc biệt O(c) = O(1)

BÀI TẬP

Bài 1: Tính thời gian thực hiện của các đọan chương trình sau: a) Tính tổng của các số

{ scanf(&n); sum = sum + x; } b) Tính tích hai ma trận vuông cấp n C = A*B

Bài 2: Giải các phương trình đệ quy sau với T(1) = 1 và: a) T(n) = T(n/2) +1 b) T(n) = 2T(n/2) + logn c) T(n) = 2T(n/2) + n

Bài 3: Giải các phương trình đệ quy sau với T(1) = 1 và: a) T(n) = 4T(n/3) + n b) T(n) = 4T(n/3) + n 2

Bài 4: Giải các phương trình đệ quy sau với T(1) = 1 và: a) T(n) = 3T(n/2) + n b) T(n) = 3T(n/2) + n 2 c) T(n) = 8T(n/2) + n 3

Bài 5: Xét định nghĩa số tổ hợp chập k của n nhƣ sau:

1 a) Viết một hàm đệ quy để tính số tổ hợp chập k của n b) Tính thời gian thực hiện của giải thuật nói trên nếu k = 0 hoặc k n nếu 0 < k < n

CÁC KIỂU DỮ LIỆU NÂNG CAO

MẢNG

Mảng 1 chiều là một cấu trúc dữ liệu bao gồm một số cố định các phần tử có kiểu giống nhau đƣợc tổ chức thành một dãy tuần tự các phần tử Nhƣ vậy mảng một chiều là một cấu trúc dữ liệu có kích thước cố định và đồng nhất a Sự đặc tả cú pháp:

Các thuộc tính của 1 mảng là:

Số lượng các phần tử trong một miền giá trị luôn được xác định rõ ràng bằng cách cung cấp miền giá trị của các chỉ số Thông thường, miền giá trị này là một miền con của các số nguyên, giúp dễ dàng tính toán số phần tử Trong trường hợp này, số lượng phần tử được xác định bằng công thức: cuối cùng trừ đi đầu tiên rồi cộng thêm 1, đảm bảo tính đúng số phần tử trong miền.

- Kiểu dữ liệu của mỗi một phần tử

Chỉ số được sử dụng để lựa chọn từng phần tử trong tập hợp, giúp xác định chính xác vị trí của các phần tử Khi tập chỉ số được xác định bởi một miền con của tập các số nguyên, nó cho phép lựa chọn các phần tử theo thứ tự rõ ràng Mỗi chỉ số trong miền con này tương ứng với một phần tử cụ thể, ví dụ như phần tử đầu tiên hoặc phần tử thứ hai trong tập hợp Việc sử dụng chỉ số này là phương pháp hiệu quả để thao tác và xử lý dữ liệu trong các thuật toán và ứng dụng toán học.

Khai báo mảng trong C là: tên kiểu tên mảng [số phần tử]

Khai báo này xác định mảng msn có 10 phần tử là các số nguyên Các phần tử này đƣợc lựa chọn bởi các chỉ số từ 0 đến 9

Miền giá trị của chỉ số không nhất thiết bắt đầu từ 1

Miền giá trị của chỉ số không nhất thiết phải là một miền con của số nguyên, mà có thể là bất kỳ một liệt kê nào, hoặc thậm chí là một miền con của một liệt kê đó Điều này giúp mở rộng phạm vi áp dụng của các chỉ số trong các lĩnh vực như toán học và lập trình, mang lại tính linh hoạt cao hơn trong việc xác định các tập hợp giá trị hợp lệ Hiểu rõ về khái niệm này sẽ giúp các nhà phát triển và nhà toán học tối ưu hóa các thuật toán và mô hình của mình một cách hiệu quả hơn.

Các phép tóan trên mảng bao gồm:

Phép toán lựa chọn phần tử trong mảng được thực hiện thông qua việc lấy chỉ số, viết dưới dạng tên của mảng theo sau là chỉ số của phần tử, ví dụ như V[1] hoặc X[ba] Chỉ số có thể là một hằng số, biến hoặc một biểu thức phức tạp như v[i] hoặc v[i+2], giúp lập trình trở nên linh hoạt và đơn giản hơn nhờ tính khái quát của biểu thức chỉ số Nhờ đó, việc in ra giá trị của nhiều phần tử trong mảng, chẳng hạn 10 phần tử của mảng v, không còn phải viết nhiều lệnh lặp lại mà có thể dễ dàng thực hiện bằng biểu thức chỉ số linh hoạt.

Trong lập trình, để nhập nhiều phần tử của mảng theo kiểu scanf(v[1]), scanf(v[2]), scanf(v[3]), , ta có thể sử dụng vòng lặp for để tối ưu hóa quá trình nhập dữ liệu Cụ thể, chỉ cần viết một lệnh for (int i=0; i 0; khi n = 0 thì gọi là danh sách rỗng (empty list) Phần tử đầu tiên của danh sách là a1, còn phần tử cuối cùng là an Độ dài của danh sách là số phần tử chứa trong danh sách, phản ánh kích thước của tập hợp phần tử đó.

Một đặc điểm quan trọng của danh sách là các phần tử có thứ tự tuyến tính dựa trên vị trí của chúng Ta xác định ai đứng trước ai+1 theo thứ tự từ phần tử thứ 1 đến phần tử thứ n-1, đồng thời nói rằng ai là phần tử đứng sau ai-1 từ phần tử thứ 2 đến phần tử thứ n Ngoài ra, ta còn gọi ai là phần tử tại vị trí thứ i hoặc 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 28 đƣợc liệt kê trên giấy nhƣ sau:

Là một danh sách Danh sách này gồm có 5 phần tử, mỗi phần tử có một vị trí trong danh sách theo thứ tự xuất hiện của nó

1 Các phép toán trên danh sách Để thiết lập kiểu dữ liệu trừu tƣợng danh sách (hay ngắn gọn là danh sách) ta phải định nghĩa các phép toán trên danh sách Và nhƣ chúng ta sẽ thấy trong toàn bộ giáo trình, không có một tập hợp các phép toán nào thích hợp cho mọi ứng dụng (application) Vì vậy ở đây ta sẽ định nghĩa một số phép toán cơ bản nhất trên danh sách Để thuận tiện cho việc định nghĩa ta giả sử rằng danh sách gồm các phần tử có kiểu là kiểu phần tử (elementType); vị trí của các phần tử trong danh sách có kiểu là kiểu vị trí và vị trí sau phần tử cuối cùng trong danh sách L là ENDLIST(L) Cần nhấn mạnh rằng khái niệm vị trí (position) là do ta định nghĩa, nó không phải là giá trị của các phần tử trong danh sách Vị trí có thể là đồng nhất với vị trí lưu trữ phần tử hoặc không

Các phép toán đƣợc định nghĩa trên danh sách là:

INSERT_LIST(x,p,L): xen phần tử x (kiểu ElementType) tại vị trí p (kiểu

Trong danh sách L gồm các phần tử từ a1 đến an, phép chèn một phần tử x vào vị trí p sẽ đưa x vào sau phần tử p-1 và trước phần tử p, tạo thành danh sách mới Nếu vị trí p không tồn tại trong danh sách hoặc sai lệch về giới hạn, phép chèn không được xác định và không thực hiện được Đây là thao tác quan trọng trong xử lý danh sách liên kết hoặc mảng để duy trì tính thứ tự của các phần tử.

Chức năng LOCATE(x, L) xác định vị trí của phần tử có nội dung x đầu tiên trong danh sách L Kết quả trả về là vị trí (kiểu position) của phần tử x trong danh sách, giúp người dùng dễ dàng xác định vị trí chính xác của dữ liệu Nếu phần tử x không xuất hiện trong danh sách, hàm sẽ trả về vị trí sau phần tử cuối cùng của danh sách, tương ứng với ENDLIST(L).

RETRIEVE(p, L) là phương thức lấy giá trị của phần tử tại vị trí p trong danh sách L Nếu vị trí p không tồn tại trong danh sách, kết quả sẽ không xác định và có thể cần thông báo lỗi Đây là thao tác cơ bản giúp truy cập dữ liệu trong danh sách theo vị trí, đảm bảo hiệu quả trong xử lý dữ liệu.

Chương trình DELETE_LIST(p, L) thực hiện việc xoá phần tử tại vị trí p trong danh sách Nếu vị trí p không tồn tại trong danh sách, phép toán sẽ không được định nghĩa và danh sách L không bị thay đổi Đây là thao tác cần thiết để quản lý và chỉnh sửa danh sách một cách chính xác trong lập trình.

Hàm NEXT(p, L) trả về vị trí của phần tử đứng sau phần tử p trong danh sách L, giúp xác định phần tử kế tiếp một cách chính xác Nếu p là phần tử cuối cùng trong danh sách, hàm sẽ trả về ENDLIST(L), chỉ rõ không còn phần tử nào sau p Chức năng của Next là không xác định với những vị trí không phải là phần tử hợp lệ trong danh sách, đảm bảo tính chính xác trong xử lý danh sách liên kết.

Hàm PREVIOUS(p, L) trả về vị trí của phần tử đứng trước phần tử p trong danh sách Nếu p là phần tử đầu tiên trong danh sách, hàm này sẽ không xác định Ngoài ra, PREVIOUS cũng không xác định trong trường hợp p không phải là vị trí của bất kỳ phần tử nào trong danh sách, giúp đảm bảo tính chính xác của hoạt động xử lý danh sách liên kết.

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ề

EMPTY_LIST(L) cho kết quả TRUE nếu danh sách có rỗng, ngƣợc lại nó cho giá trị FALSE

MAKENULL_LIST(L) khởi tạo một danh sách L rỗng

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 ở đây nhƣ là các phép toán nguyên thủy

Để sắp xếp danh sách theo thứ tự tăng dần, bạn có thể dùng các phép toán trừu tượng trên danh sách để viết một chương trình con nhận một tham số là danh sách Chương trình này sẽ xử lý và sắp xếp các phần tử trong danh sách dựa trên kiểu có thứ tự của chúng Việc sử dụng các phép toán trừu tượng giúp đơn giản hóa quá trình xử lý dữ liệu và đảm bảo tính linh hoạt trong việc sắp xếp danh sách theo thứ tự mong muốn.

Chương trình con sắp xếp danh sách được thực hiện bằng cách sử dụng hàm SWAP(p, q) để đổi chỗ hai phần tử tại vị trí p và q trong danh sách Hàm SWAP giúp hoán đổi các phần tử một cách hiệu quả, đảm bảo quá trình sắp xếp diễn ra chính xác Viết đoạn mã ví dụ như sau: void SORT(LIST L) { } để thực hiện thuật toán sắp xếp dựa trên việc hoán đổi vị trí các phần tử trong danh sách.

//kiểu vị trí của các phần tử trong danh sách p= FIRST(L);

//vị trí phần tử đầu tiên trong danh sách while (p!=ENDLIST(L)){

//vị trí phần tử đứng ngay sau phần tử p while (q!=ENDLIST(L)){ if (RETRIEVE(p,L) > RETRIEVE(q,L)) swap(p,q); // dịch chuyển nội dung phần tử q=NEXT(q,L);

Các phép toán trừu tượng trong lập trình cần được cài đặt thành các chương trình con để xây dựng giải thuật chạy được Trong quá trình cài đặt, một số tham số hình thức của các phép toán này có thể không cần thiết, do đó có thể bỏ qua chúng để tối ưu hóa chương trình Ví dụ, phép toán INSERT_LIST(x, p, L) gồm ba tham số, nhưng khi cài đặt danh sách liên kết đơn bằng con trỏ, tham số L là không cần thiết vì chỉ cần thay đổi các con trỏ tại vị trí p để kết nối phần tử mới Tuy nhiên, trong bài giảng này, chúng ta vẫn giữ nguyên tất cả các tham số để đảm bảo tính rõ ràng và thống nhất trong quá trình cài đặt.

Trong cách cài đặt để làm cho chương trình đồng nhất và trong suốt đối với các phương pháp cài đặt của cùng một kiểu dữ liệu trừu tượng

2 Cài đặt danh sách bằng mảng (danh sách đặc):

Bạn có thể cài đặt danh sách bằng mảng, lưu giữ các phần tử liên tiếp từ vị trí đầu tiên của mảng Để thực hiện điều này, cần ước lượng số phần tử của danh sách để khai báo kích thước mảng phù hợp, đảm bảo mảng đủ chỗ cho tất cả các phần tử Mảng thường còn thừa một số chỗ trống để dễ mở rộng sau này Ngoài ra, cần lưu giữ độ dài hiện tại của danh sách, giúp xác định số phần tử và phần trống trong mảng, như hình minh họa trong hình II.1 Vị trí của một phần tử trong danh sách được xác định bằng chỉ số của mảng cộng thêm 1, vì phần tử đầu tiên trong mảng có chỉ số 0.

Nội dung phần tử Phần tử thứ 1 Phần tử thứ 2 Phần tử cuối cùng trong danh sách Hình II.1: Cài đặt danh sách bằng mảng

Với hình ảnh minhhọa trên, ta cần các khai báo cần thiết là

Define MaxLength as an appropriate integer to specify the maximum size of the list Use "ElementType" as the data type for the list elements, ensuring flexibility and clarity in list operations Employ "Position" as the data type for element positions within the list to facilitate navigation and management Structure the list using a well-defined struct to organize its components effectively, supporting efficient list implementation and manipulation.

ElementType Elements[MaxLength]; //mảng chứa các phần tử của danh sách Position Last; //giữ độ dài danh sách

Đây là bài trình diễn về kiểu dữ liệu trừu tượng danh sách thông qua cấu trúc dữ liệu mảng, giúp hiểu rõ cách biểu diễn danh sách trong lập trình Phần tiếp theo sẽ giới thiệu cách cài đặt các phép toán cơ bản trên danh sách, như thêm, xóa, tìm kiếm và cập nhật phần tử, nhằm xây dựng kiến thức vững chắc về quản lý dữ liệu trong lập trình.

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

CÀI ĐẶT DANH SÁCH BẰNG CON TRỎ (DANH SÁCH LIÊN KẾT) 36 I NGĂN XẾP (STACK)

Cách khác để cài đặt danh sách là dùng con trỏ để liên kết các ô chứa các

Trong phương pháp này, danh sách gồm 37 phần tử được lưu trữ trong các ô riêng biệt, mỗi ô giữ một phần tử và có thể trỏ tới ô chứa phần tử kế tiếp Cơ chế này giúp quản lý danh sách một cách hiệu quả, dễ dàng thao tác và truy cập các phần tử liên tiếp trong danh sách liên kết Việc sử dụng các ô liên kết này phù hợp với các ứng dụng yêu cầu quản lý dữ liệu linh hoạt, tối ưu hóa bộ nhớ và nâng cao hiệu suất xử lý dữ liệu.

Trong một lớp học gồm bốn bạn là Đông, Tây, Nam và Bắc, các địa chỉ lần lượt là d, t, n, b Đông có địa chỉ của Nam, trong khi Tây không có địa chỉ của bạn nào, còn Bắc giữ địa chỉ của Đông Ngoài ra, Nam có địa chỉ của Tây, như hình II.2 đã minh họa.

Hình II.2 minh họa rằng, khi xem xét thứ tự các phần tử theo cơ chế chỉ đến, ta có danh sách gồm Bắc, Đông, Nam, Tây Quá trình này yêu cầu duy trì địa chỉ của phần tử Bắc để đảm bảo danh sách được xác định chính xác Điều này cho thấy vai trò của việc giữ địa chỉ của phần tử Bắc trong việc xây dựng thứ tự các phần tử theo cơ chế chỉ đến.

Trong cài đặt, để một ô có thể chỉ đến ô khác ta cài đặt mỗi ô là một mẩu tin

Danh sách liên kết đơn là cấu trúc dữ liệu gồm các phần tử được liên kết với nhau thông qua con trỏ Mỗi phần tử, gọi là record hoặc struct, chứa trường Element giữ giá trị của phần tử và trường Next là con trỏ trỏ đến phần tử kế tiếp; trường Next của phần tử cuối trỏ tới NULL, thể hiện kết thúc danh sách Quản lý danh sách đơn giản bằng cách sử dụng một biến Header trỏ tới ô chứa phần tử đầu tiên, giúp dễ dàng thao tác và truy cập danh sách Trong cài đặt, Header có cấu trúc giống các phần tử, nhưng không chứa dữ liệu thực, chỉ có con trỏ Next trỏ tới phần tử đầu tiên của danh sách; nếu danh sách rỗng, Header sẽ trỏ đến NULL.

Trong cấu trúc danh sách liên kết, ô Header được cấp phát bộ nhớ như một ô chứa dữ liệu để đơn giản hóa các thuật toán thêm, xoá phần tử Việc phân biệt giữa giá trị của phần tử và vị trí của nó trong danh sách là rất quan trọng; ví dụ, giá trị của phần tử đầu tiên là a1, còn vị trí của nó là địa chỉ của ô chứa nó nằm trong trường next của ô Header Giá trị và vị trí của các phần tử trong danh sách liên kết được thể hiện rõ ràng qua hình ảnh minh họa.

Hình II.3 Danh sách liên kết đơn a1 a2 … an NUL

Trong danh sách liên kết, vị trí của phần tử thứ i được xác định là ô đứng trước nó, tức là ô thứ (i-1) Để truy cập vào phần tử thứ i, ta cần truy xuất ô (i-1), và mỗi thao tác thêm hoặc xóa phần tử tại vị trí p đều yêu cầu cập nhật lại con trỏ trỏ tới vị trí này, chính là ô (p-1) Nói cách khác, để thao tác tại vị trí p, ta cần biết con trỏ trỏ tới ô chứa phần tử p, chính là ô (p-1) Vị trí của phần tử thứ i được định nghĩa là con trỏ trỏ tới ô lưu trữ phần tử đó, với phần tử đầu tiên nằm trỏ tới Header, phần tử thứ hai trỏ tới ô chứa a1, phần tử thứ ba trỏ tới ô chứa a2, và cứ tiếp tục như vậy cho đến phần tử thứ n.

1 Vậy vị trí sau phần tử cuối trong danh sách, tức là ENDLIST, chính là con trỏ trỏ ô chứa phần tử an (xem hình II.3)

Trong danh sách, nếu p là vị trí của phần tử thứ p, thì giá trị của phần tử ở vị trí p này nằm trong trường element của ô được trỏ bởi p- Điều này giúp xác định rõ vị trí và giá trị của phần tử trong danh sách, đảm bảo tính chính xác trong quá trình xử lý dữ liệu Hiểu rõ nguyên tắc này là chìa khóa để tối ưu hóa các thao tác xử lý danh sách và đảm bảo dữ liệu được lưu trữ đúng cách.

>next Nói cách khác p->next->element chứa nội dung của phần tử ở vị trí p trong danh sách

Phần tử thứ Giá trị Vị trí

Sau phần tử cuối cùng Không xác định N và n->next có giá trị là

Các khai báo cần thiết là typedef ElementType; //kiểu của phần tử trong danh sách typedef struct Node{

ElementType Element; //Chứa nội dung của phần tử

Node* Next; /*con trỏ chỉ đến phần tử kế tiếp trong danh sách*/

}; typedef Node* Position; // Kiểu vị trí typedef Position List;

Trong cấu trúc danh sách liên kết, Header được hiểu như một biến con trỏ có kiểu giống ô chứa phần tử trong danh sách Tuy nhiên, trường Element của Header không bao giờ được sử dụng, chỉ có trường Next mới đóng vai trò quan trọng để trỏ tới ô chứa phần tử đầu tiên của danh sách Khi danh sách liên kết rỗng, trường ô Header sẽ không trỏ tới ô nào, biểu thị danh sách chưa chứa phần tử nào.

Trong quá trình khởi tạo danh sách liên kết rỗng, cần phải cấp phát bộ nhớ cho ô Header và đảm bảo con trỏ trong trường next của nó trỏ tới NULL, vì ô thứ 39 vẫn tồn tại nhưng không chứa phần tử nào Điều này giúp danh sách duy trì trạng thái rỗng và sẵn sàng cho các thao tác thêm, xóa phần tử sau này Hàm MakeNull_List giúp khởi tạo danh sách liên kết một cách chính xác bằng cách thiết lập con trỏ next của Header trỏ về NULL.

(*Header)=(Node*)malloc(sizeof(Node));

Kiểm tra một danh sách rỗng

Danh sách rỗng nếu như trường next trong ô Header trỏ tới NULL int Empty_List(List L){ return (L->Next==NULL);}

Xen một phần tử vào danh sách :

Khi chèn một phần tử có giá trị x vào danh sách liên kết tại vị trí p, cần phải cấp phát một ô nhớ mới để lưu trữ phần tử này Sau đó, các con trỏ liên kết được cập nhật nhằm đưa ô mới này vào đúng vị trí p trong danh sách Quá trình nối kết và thứ tự các thao tác cụ thể đã được trình bày rõ ràng trong Hình II.4.

Hình II.4: Thêm một phần tử vào danh sách tại vị trí p void Insert_List(ElementType X, Position P, List *L)

{ Position T; T=(Node*)malloc(sizeof(Node)); T->Element=X;

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

Hình II.5: Xoá phần tử tại vị trí p

Để xóa một phần tử khỏi danh sách liên kết, ta cần xác định vị trí của phần tử đó trong danh sách Việc kết nối các con trỏ được thực hiện bằng cách cho con trỏ của phần tử đứng trước trỏ tới phần tử đứng sau nó Trong các ngôn ngữ lập trình không có cơ chế thu hồi vùng nhớ tự động như Pascal hay C, ta phải thực hiện thủ công việc giải phóng bộ nhớ của ô bị xóa để tránh rò rỉ bộ nhớ Tuy nhiên, do tính đơn giản của thuật toán, trong nhiều trường hợp, việc thu hồi vùng nhớ không được đề cập rõ ràng Các thao tác chi tiết khi xóa một phần tử trong danh sách liên kết được trình bày trong hình II.5, và có thể được cài đặt trong chương trình con như hàm Delete_List với cú pháp: void Delete_List(Position P, List *L){ Position T; if (P->Next!=NULL){.

T=P->Next; /*/giữ ô chứa phần tử bị xoá để thu hồi vùng nhớ*/

P->Next=T->Next; /*nối kết con trỏ trỏ tới phần tử thứ p+1*/ free(T); //thu hồi vùng nhớ

Để định vị phần tử x trong danh sách liên kết, ta bắt đầu tìm từ đầu danh sách Nếu tìm thấy x, hàm sẽ trả về vị trí của phần tử đầu tiên chứa x; nếu không tìm thấy, hàm sẽ trả về ENDLIST(L) Trong trường hợp x có trong danh sách, hàm Locate sẽ trả về vị trí p sao cho x bằng p->next->element.

Position Locate(ElementType X, List L){ Position P; int Found = 0; P = L; while ((P->Next != NULL) && (Found == 0)) if (P->Next->Element == X) Found = 1; else P = P->Next; return P;

Hàm Locate cho phép truyền vào giá trị L có thể là bất kỳ giá trị nào Nếu L là Header, hàm sẽ tìm x bắt đầu từ đầu danh sách; còn nếu L là một vị trí p bất kỳ, hàm sẽ định vị phần tử x bắt đầu từ vị trí p Điều này giúp mở rộng khả năng tìm kiếm linh hoạt trong danh sách liên kết.

Xác định nội dung phần tử:

Nội dung phần tử đang lưu trữ tại vị trí p trong danh sách L là p->next-

Hàm sẽ trả về giá trị của p->next->element nếu phần tử đó tồn tại trong danh sách liên kết Nếu p->next bằng NULL, nghĩa là phần tử không tồn tại, do đó hàm không xác định giá trị trả về Việc kiểm tra trạng thái của p->next là rất quan trọng để đảm bảo an toàn khi thao tác với danh sách liên kết This approach giúp tránh lỗi truy cập bộ nhớ không hợp lệ trong quá trình xử lý dữ liệu.

41 if (P->Next!=NULL) return P->Next->Element;

Ngăn xếp (Stack) là cấu trúc dữ liệu dạng danh sách, trong đó việc thêm hoặc loại bỏ phần tử chỉ diễn ra tại một đầu gọi là đỉnh (TOP) của ngăn xếp Đây là một trong những cấu trúc dữ liệu cơ bản phổ biến, được sử dụng rộng rãi trong lập trình và các thuật toán để quản lý dữ liệu một cách hiệu quả Ngăn xếp hoạt động theo nguyên tắc Last-In-First-Out (LIFO), nghĩa là phần tử được đưa vào sau cùng sẽ là phần tử đầu tiên được lấy ra Cấu trúc này phù hợp cho các ứng dụng cần xử lý theo thứ tự ngược lại, như duyệt cây, quay ngược dữ liệu hoặc quản lý các trạng thái.

HÀNG ĐỢI (QUEUE)

Hàng đợi (queue) là một danh sách đặc biệt trong đó phép thêm phần tử chỉ được thực hiện tại cuối hàng (REAR), còn phép loại bỏ diễn ra ở đầu hàng (FRONT) Mô hình xếp hàng mua vé xem phim minh họa rõ nét cho khái niệm này, khi người mới đến sẽ thêm vào cuối hàng, trong khi người đã ở đầu hàng sẽ mua vé và rời đi Do đó, hàng đợi còn được gọi là cấu trúc FIFO (First-In-First-Out), nghĩa là "vào trước, ra trước".

Bây giờ chúng ta sẽ thảo luận một vài phép toán cơ bản nhất trên hàng

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) hàm trả về phần tử đầu tiên của hàng Q

ENQUEUE(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) hàm kiểm tra hàng rỗng

FULL_QUEUE(Q) kiểm tra hàng đầy

Trong phần ngăn xếp, có thể sử dụng danh sách để biểu diễn một hàng và áp dụng các phép toán đã cài đặt để thao tác trên hàng, nhưng phương pháp này có thể không tối ưu Việc dùng danh sách bằng mảng dẫn đến các thao tác như chèn phần tử vào cuối danh sách hay xóa phần tử đầu hàng gặp phải hạn chế về hiệu suất, vì thao tác chèn mất thời gian cố định còn thao tác xóa đòi hỏi di chuyển toàn bộ phần tử trong mảng, gây tốn thời gian tỷ lệ với số phần tử trong hàng Để cải thiện hiệu quả, cần suy nghĩ dựa trên tính chất đặc biệt của phép thêm và loại bỏ phần tử trong hàng, từ đó tìm ra các cách cài đặt tối ưu hơn.

46 a Cài đặt hàng bằng mảng

Trong cấu trúc dữ liệu, ta sử dụng một mảng để lưu trữ các phần tử của hàng, với phần tử đầu tiên được đưa vào vị trí thứ 1 của mảng, phần tử thứ 2 vào vị trí thứ 2, và cứ tiếp tục như vậy Khi hàng có n phần tử, ta đặt chỉ số front bằng 0 và rear bằng n-1 Quá trình xóa phần tử sẽ tăng giá trị của front lên 1, còn việc thêm phần tử mới sẽ làm rear tăng lên 1, cho thấy hàng có hướng đi xuống Nếu một lúc nào đó rear đạt đến vị trí maxLength - 1 mà vẫn còn chỗ trống trong mảng (các vị trí trước front), ta gọi là hàng bị tràn, hay còn gọi là hàng bị đầy khi toàn bộ mảng đã chứa các phần tử của hàng.

Để khắc phục hàng đợi bị tràn tại vị trí t-1, chúng ta áp dụng phương pháp di chuyển tịnh tiến Trong trường hợp này, front luôn nhỏ hơn hoặc bằng rear, nếu hàng chưa đầy ta sẽ thêm phần tử mới vào vị trí 0 của mảng, rồi tiếp tục thêm phần tử mới vào vị trí 1 (nếu còn chỗ) Phương pháp này cho phép front có thể lớn hơn rear Cách khắc phục hiệu quả nhất là sử dụng mảng xoay vòng, giúp quản lý hàng đợi một cách tối ưu và tránh tràn bộ nhớ.

Hàng tràn Hàng sau khi dịch chuyển tịnh tiến

Hình II.7: Minh họa việc di chuyển tịnh tiến các phần tử khi hàng bị tràn

Để cài đặt hàng bằng mảng theo phương pháp tịnh tiến, ta chỉ cần quản lý vị trí đầu hàng và cuối hàng Sử dụng hai biến số để chỉ định vị trí của phần tử đầu tiên và phần tử cuối cùng trong hàng, giúp đơn giản hóa quá trình thao tác và nâng cao hiệu quả quản lý dữ liệu Phương pháp này tối ưu cho việc thực hiện các thao tác như thêm hoặc xóa phần tử, đảm bảo tính linh hoạt và linh động của hàng mảng.

Các khai báo cần thiết

#define MaxLength //chiều dài tối đa của mảng typedef ElementType; //Kiểu dữ liệu của các phần tử trong hàng typedef struct {

ElementType Elements[MaxLength]; //Lưu trữ nội dung các phần tử int Front, Rear; //chỉ số đầu và đuôi hàng

Khi cả front và rear không trỏ đến vị trí hợp lệ trong mảng, chúng ta có thể đặt cả hai bằng -1 để biểu thị hàng đợi rỗng Hàm MakeNull_Queue giúp thiết lập lại trạng thái của hàng đợi bằng cách gán giá trị -1 cho các chỉ số Front và Rear, đảm bảo quản lý bộ nhớ hiệu quả và tránh lỗi truy cập Việc sử dụng giá trị -1 để đánh dấu hàng đợi rỗng là một phương pháp phổ biến trong lập trình hàng đợi, góp phần tối ưu hóa hoạt động và duy trì tính hợp lệ của cấu trúc dữ liệu.

Trong quá trình làm việc với hàng, việc thêm và xóa các phần tử là rất phổ biến Khi một phần tử được đưa vào hàng, giá trị của front sẽ lớn hơn -1, và khi xóa một phần tử, front sẽ tăng thêm 1 Hàng được xem là rỗng khi mà front > rear hoặc khi hàng chưa được khởi tạo, tức là front = -1 Để kiểm tra hàng rỗng một cách đơn giản, ta sẽ đặt lại front = -1 khi phần tử duy nhất trong hàng bị xóa Do đó, hàng rỗng chỉ khi và chỉ khi front = -1, điều này giúp xác định chính xác trạng thái rỗng của hàng.

Hàng đầy nếu số phần tử hiện có trong hàng bằng số phần tử trong mảng int Full_Queue(Queue Q){ return (Q.Rear-Q.Front+1)==MaxLength;

Xóa phần tử ra khỏi hàng

Khi xóa một phần tử từ hàng đợi, chỉ cần tăng giá trị của front lên 1 Nếu giá trị của front lớn hơn rear, điều đó có nghĩa là hàng đã rỗng, và chúng ta cần khởi tạo lại hàng đợi bằng cách đặt lại cả hai giá trị front và rear về -1 để đảm bảo dữ liệu được quản lý chính xác.

Q->Front=Q->Front+1; if (Q->Front>Q->Rear) MakeNull_Queue(Q);

48 else printf("Loi: Hang rong!");

Thêm phần tử vào hàng

Khi một phần tử được thêm vào hàng, nó sẽ nằm ở vị trí kế vị trí Rear cũ của hàng, đảm bảo tính liên tục của hàng đợi Quá trình thêm phần tử vào hàng đòi hỏi phải xem xét các trường hợp khác nhau để duy trì cấu trúc và thứ tự của hàng đợi một cách hợp lý Việc này giúp đảm bảo các thao tác enqueue và dequeue hoạt động chính xác, tối ưu hóa hiệu suất của hàng đợi trong các ứng dụng.

Nếu hàng đầy thì báo lỗi không thêm đƣợc nữa

Trong quá trình thêm phần tử vào hàng đợi, cần kiểm tra xem hàng đã đầy chưa Nếu hàng bị tràn, ta phải di chuyển các phần tử để tạo khoảng trống rồi mới đưa phần tử mới vào cuối hàng, với phần tử cuối (rear) tăng lên 1 Đặc biệt, khi hàng đợi đang rỗng, ta cần đặt giá trị của front = 0 để trỏ đúng vào phần tử đầu tiên của hàng Hàm EnQueue đảm bảo thực hiện các bước này một cách chính xác, giúp duy trì tính liên tục và đúng chuẩn của cấu trúc dữ liệu hàng đợi.

//Di chuyen tinh tien ra truoc Front -1 vi tri for(int i=Q->Front;iRear;i++)

Q->Rear=MaxLength - Q->Front-1; //Xac dinh vi tri Rear moi

Q->Rear=Q->Rear+1; //Tang Rear de luu noi dung moi

} else printf("Loi: Hang day!");

} b Cài đặt hàng với mảng xoay vòng

Hình II.8: Cài đặt hàng bằng mảng xoay vòng

Phương pháp này xử lý hàng bị tràn khi rear bằng maxlength - 1 mà chưa đầy, tức là front > 0, bằng cách thêm phần tử mới vào vị trí 0 của mảng Sau đó, tiếp tục lặp lại quá trình này vì các vị trí từ 0 đến front - 1 vẫn còn trống Vì sử dụng mảng theo kiểu xoay vòng, phương pháp này được gọi là phương pháp dùng mảng xoay vòng để quản lý hàng đợi một cách hiệu quả.

Các phần khai báo cấu trúc dữ liệu, tạo hàng rỗng, kiểm tra hàng rỗng giống như phương pháp di chuyển tịnh tiến

#define MaxLength //chiều dài tối đa của mảng typedef ElementType; //Kiểu dữ liệu của các phần tử trong hàng typedef struct {

ElementType Elements[MaxLength]; //Lưu trữ nội dung các phần tử int Front, Rear; //chỉ số đầu và đuôi hàng

Lúc này front và rear không trỏ đến vị trí hợp lệ nào trong mảng vậy ta có thể cho front và rear đều bằng -1 void MakeNull_Queue(Queue *Q){

Kiểm tra hàng rỗng int Empty_Queue(Queue Q){ return Q.Front = = -1;

Hàng đầy xảy ra khi tất cả các ô trong mảng đều chứa các phần tử của hàng, cho thấy bộ hàng đầy và không còn ô trống để thêm phần tử mới Với phương pháp này, chỉ số front có thể lớn hơn rear mà vẫn đảm bảo tính hợp lệ của hàng đợi, do đó dễ dàng xử lý các trường hợp hàng đầy mà không bị giới hạn bởi thuật toán truyền thống Có hai trường hợp hàng đầy thường gặp là khi các chỉ số front và rear chạm vào nhau hoặc khi hàng bị đầy một cách liên tục mà không có ô trống nào để tiếp tục thêm phần tử mới, giúp tối ưu hóa hoạt động của hàng đợi trong các ứng dụng thực tế.

- Trường hợp Q.Rear=Maxlength-1 và Q.Front =0

Trong trường hợp Q.Front = Q.Rear + 1, ta có thể đơn giản hóa bằng cách viết công thức chung: (Q.rear - Q.front + 1) mod MaxLength = 0 Để kiểm tra xem hàng đợi đã đầy hay chưa, ta sử dụng hàm Full_Queue với biểu thức: return (Q.Rear - Q.Front + 1) % MaxLength == 0 Công thức này giúp xác định chính xác trạng thái đầy của hàng đợi vòng trong các thuật toán liên quan.

Xóa một phần tử ra khỏi ngăn xếp

Khi xóa một phần tử ra khỏi hàng, ta xóa tại vị trí đầu hàng và có thể xảy ra các trường hợp sau:

Nếu hàng rỗng thì báo lỗi không xóa;

Ngƣợc lại, nếu hàng chỉ còn 1 phần tử thì khởi tạo lại hàng rỗng;

Ngƣợc lại, thay đổi giá trị của Q.Front

(Nếu Q.front != Maxlength-1 thì đặt lại Q.front = q.Front +1; Ngƣợc lại Q.front=0) void DeQueue(Queue *Q){ if (!Empty_Queue(*Q)){

//Nếu hàng chỉ chứa một phần tử thì khởi tạo hàng lại if (Q->Front==Q->Rear) MakeNull_Queue(Q); else Q->Front=(Q->Front+1) % MaxLength; //tăng Front lên 1 đơn vị

} else printf("Loi: Hang rong!");

Thêm một phần tử vào hàng

Khi thêm một phần tử vào hàng thì có thể xảy ra các trường hợp sau:

- Trường hợp hàng đầy thì báo lỗi và không thêm;

To implement a circular queue, update the rear index by setting Q.rear to 0 if it reaches the maximum size minus one; otherwise, increment Q.rear by 1 Subsequently, insert the new element at the updated Q.rear position The EnQueue function first checks if the queue is full; if not, it initializes the front index to 0 when the queue is empty before adding the new element This approach ensures efficient queue management and prevents overflow errors.

} else printf("Loi: Hang day!");

} c Cài đặt hàng bằng danh sách liên kết (cài đặt bằng con trỏ)

Để duyệt một hàng một cách tự nhiên và hiệu quả, bạn có thể sử dụng hai con trỏ front và rear để trỏ tới phần tử đầu tiên và cuối cùng của hàng Phần hàng này được cài đặt như một danh sách liên kết có Header là một ô thực sự, giúp quá trình truy cập và xử lý dữ liệu trở nên dễ dàng hơn (Xem hình II.9 để hình dung rõ hơn về cấu trúc này.)

51 typedef ElementType; //kiểu phần tử của hàng typedef struct Node{

Node* Next; //Con trỏ chỉ ô kế tiếp

}; typedef Node* Position; typedef struct{

Position Front, Rear; //là hai trường chỉ đến đầu và cuối của hàng

Khi hàng rỗng Front va Rear cùng trỏ về 1 vị trí đó chính là ô header

Hình II.9: Khởi tạo hàng rỗng void MakeNullQueue(Queue *Q){

Header=(Node*)malloc(sizeof(Node)); //Cấp phát Header

Header->Next=NULL; Q->Front=Header;

Hàng rỗng nếu Front và Rear chỉ cùng một vị trí là ô Header int EmptyQueue(Queue Q){ return (Q.Front==Q.Rear);

Thêm một phần tử vào hàng

MỘT SỐ ỨNG DỤNG CỦA DANH SÁC H

Đảo ngược xâu ký tự là bài toán yêu cầu hiển thị các ký tự của một chuỗi theo thứ tự ngược lại Kết quả là ký tự cuối cùng trong xâu sẽ được hiển thị đầu tiên, tiếp theo là ký tự sát ký tự cuối, và cuối cùng là ký tự đầu tiên sẽ xuất hiện ở cuối chuỗi Đây là phép đảo ngược đơn giản nhưng quan trọng trong xử lý chuỗi ký tự trong lập trình.

Chuỗi đảo ngược sử dụng ngăn xếp là một ứng dụng đơn giản và hiệu quả, phù hợp với đặc tính của cấu trúc dữ liệu này Quá trình thực hiện gồm duyệt từ đầu đến cuối chuỗi, lần lượt đưa các ký tự vào ngăn xếp, đảm bảo ký tự đầu tiên được thêm vào trước, các ký tự tiếp theo theo thứ tự Khi tất cả các ký tự đã được đưa vào ngăn xếp, ta lần lượt lấy chúng ra để hiển thị, tận dụng tính chất của ngăn xếp là phần tử cuối cùng vào trước sẽ ra trước Nhờ đó, ký tự cuối cùng của chuỗi ban đầu sẽ được lấy ra đầu tiên, và ký tự đầu tiên sẽ ở cuối, giúp đảo ngược hoàn toàn thứ tự các ký tự trong chuỗi ban đầu.

Mã chương trình đảo ngược xâu ký tự như sau:

#include void main ( ){ char *st; int i; stack *s; clrscr();

MakeNull_Stack(s); printf("Nhap vao xau ky tu: "); gets(st); for (i=0;i

Ngày đăng: 29/12/2022, 15:19

Nguồn tham khảo

Tài liệu tham khảo Loại Chi tiết
[4] Aho, Hopcroft &amp; Ullman, Data Structures and Algorithms,Addison Wesley, 2001 Sách, tạp chí
Tiêu đề: Data Structures and Algorithms
[5] Robert Sedewick, Algorithms in Java Third Edition,Addison Wesley, 2002 Sách, tạp chí
Tiêu đề: Algorithms in Java Third Edition
[6] Niklaus Wirth, Data Structures and Algorithms,Prentice Hall, 2004 Sách, tạp chí
Tiêu đề: Data Structures and Algorithms
[7] Robert Sedewick, Algorithms,Addison Wesley, 1983 Sách, tạp chí
Tiêu đề: Algorithms
[1] Nhập môn cơ sở dữ liệu quan hệ - nhà xuất bản khoa học và kỹ thuật – năm 2002 Khác
[2] Cơ sở dữ liệu – Phương Lan, Nguyễn Thiên Băng – nhà xuất bản lao động và xã hội – Năm 2005 Khác
[3] Giáo trình nhập môn cơ sở dữ liệu – Phương Lan – nhà xuất bản lao động xã hội – năm 2006 Khác
[8] Bruno R. Preiss, Data Structures and Algorithms with Object-Oriented Design, Jon Wiley &amp; Sons, 1998 Khác

TỪ KHÓA LIÊN QUAN

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

w