Có phải các cây được tổ chức bởi một cấu trúc đầu tiên của con, nextsibling? Nếu không, tai sao không?


12

Thông thường, các cấu trúc dữ liệu cây được tổ chức theo cách mà mỗi nút chứa con trỏ tới tất cả các phần tử con của nó.

       +-----------------------------------------+
       |        root                             | 
       | child1            child2         child3 |
       +--+------------------+----------------+--+
          |                  |                |
+---------------+    +---------------+    +---------------+
|    node1      |    |     node2     |    |     node3     |
| child1 child2 |    | child1 child2 |    | child1 child2 |
+--+---------+--+    +--+---------+--+    +--+---------+--+
   |         |          |         |          |         |

Điều này có vẻ tự nhiên, nhưng nó đi kèm với một số vấn đề. Ví dụ, khi số lượng nút con thay đổi, bạn cần một cái gì đó như một mảng hoặc danh sách để quản lý các con.

Thay vào đó, chỉ sử dụng con trỏ (đầu tiên) và con trỏ (tiếp theo), chúng ta sẽ có được thứ gì đó trông như thế:

       +-------------------+
       |        root       |
       | child    sibling  +--->NULL
       +--+----------------+
          |             
+----------------+    +----------------+    +----------------+
|    node1       |    |     node2      |    |     node3      |
| child  sibling +--->| child  sibling +--->| child  sibling +--->NULL
+--+-------------+    +--+-------------+    +--+-------------+
   |                     |                     |

Rõ ràng, loại cấu trúc này cũng có thể đại diện cho cây, nhưng nó cũng cung cấp một số lợi thế. Quan trọng nhất là chúng ta không phải lo lắng về số lượng các nút con nữa. Khi được sử dụng cho cây phân tích cú pháp, nó cung cấp một biểu diễn tự nhiên cho một thuật ngữ như "a + b + c + d + e" mà không trở thành một cây sâu.

Các thư viện bộ sưu tập có cung cấp các cấu trúc cây như vậy không? Do trình phân tích cú pháp sử dụng cấu trúc như vậy? Nếu không, những lý do là gì?


2
Vâng, cấu trúc này rõ ràng có chi phí phức tạp cao hơn. Điều đó chỉ đáng giá nếu bạn thực sự cần một số lượng trẻ em khác nhau. Nhiều cây có số lượng trẻ em cố định (hoặc ít nhất là tối đa cố định) vốn có trong thiết kế của chúng. Trong những trường hợp đó, các chỉ định bổ sung không thêm bất kỳ giá trị nào.
Joachim Sauer

4
Đưa các mục vào danh sách liên kết sẽ giới thiệu một O(n)yếu tố trong thuật toán.

Và để đến được nút3 từ root, bạn cần lấy cddar của root ...
Tacroy

Tacroy: Chính xác, việc tìm lại root không thực sự dễ dàng, nhưng nếu tôi thực sự cần điều đó, một con trỏ trở lại sẽ bị khủng bố (mặc dù nó sẽ làm hỏng sơ đồ ;-)
user281377

Câu trả lời:


7

Cây, giống như danh sách, là "kiểu dữ liệu trừu tượng" có thể được thực hiện theo những cách khác nhau. Mỗi cách đều có ưu điểm và nhược điểm.

Trong ví dụ đầu tiên, ưu điểm chính của cấu trúc này là bạn có thể truy cập bất kỳ đứa trẻ nào trong O (1). Nhược điểm là việc nối thêm một đứa trẻ đôi khi có thể tốn kém hơn một chút khi mảng phải được mở rộng. Chi phí này là tương đối nhỏ mặc dù. Nó cũng là một trong những thực hiện đơn giản nhất.

Trong ví dụ thứ hai, ưu điểm chính là bạn luôn nối thêm một đứa trẻ trong O (1). Nhược điểm chính là truy cập ngẫu nhiên vào một đứa trẻ có giá O (n). Ngoài ra, thể ít thú vị hơn đối với các cây khổng lồ vì hai lý do: nó có chi phí bộ nhớ của một tiêu đề đối tượng và hai con trỏ trên mỗi nút và các nút được trải ngẫu nhiên trên bộ nhớ có thể gây ra nhiều sự hoán đổi giữa bộ đệm CPU và bộ nhớ khi cây đi qua, làm cho việc thực hiện này ít hấp dẫn hơn đối với chúng. Đây không phải là một vấn đề cho các cây và ứng dụng bình thường mặc dù.

Một khả năng thú vị cuối cùng không được đề cập là lưu trữ toàn bộ cây trong một mảng duy nhất. Điều này dẫn đến mã phức tạp hơn, nhưng đôi khi là một triển khai rất thuận lợi trong các trường hợp cụ thể, đặc biệt là đối với các cây cố định rất lớn, vì bạn có thể tiết kiệm chi phí của tiêu đề đối tượng và phân bổ bộ nhớ liền kề.


1
Ví dụ: một cây B + sẽ không bao giờ sử dụng cấu trúc "con đầu lòng, mối quan hệ đầu tiên" này. Nó sẽ không hiệu quả đến mức vô lý đối với cây dựa trên đĩa và vẫn rất kém hiệu quả đối với cây dựa trên bộ nhớ. Một cây R trong bộ nhớ có thể chịu đựng được cấu trúc này, nhưng nó vẫn bao hàm nhiều lỗi nhớ cache hơn. Tôi khó có thể nghĩ đến một tình huống mà "đứa con đầu lòng, đứa con tinh nghịch" sẽ vượt trội hơn. Vâng, vâng, nó có thể hoạt động cho một cây cú pháp như ammoQ đã đề cập. Còn gì nữa không?
Qwertie

3
"Bạn luôn nối thêm một đứa trẻ vào O (1)" - Tôi nghĩ rằng bạn luôn có thể chèn một đứa trẻ ở chỉ số 0 trong O (1), nhưng việc thêm một đứa trẻ dường như rõ ràng là O (n).
Scott Whitlock

Lưu trữ toàn bộ cây trong một mảng là phổ biến cho đống.
Brian

1
@Scott: tốt, tôi giả sử danh sách được liên kết cũng chứa một con trỏ / tham chiếu đến mục cuối cùng, điều này sẽ làm cho O (1) cho vị trí đầu tiên hoặc cuối cùng ... mặc dù nó bị thiếu trong ví dụ OP
dagnelies

Tôi cá rằng (ngoại trừ có thể trong các trường hợp cực kỳ thoái hóa), việc thực hiện đầu tiên, nextsibling, không bao giờ hiệu quả hơn so với triển khai bảng con dựa trên mảng. Cache địa phương thắng, thời gian lớn. Cây B đã được chứng minh là triển khai hiệu quả nhất từ ​​trước đến nay trên các kiến ​​trúc hiện đại, chiến thắng trước các cây đen được sử dụng theo truyền thống chính xác là do địa phương bộ nhớ cache được cải thiện.
Konrad Rudolph

2

Hầu như mọi dự án có một số mô hình hoặc tài liệu có thể chỉnh sửa sẽ có cấu trúc phân cấp cho nó. Nó có thể có ích để thực hiện 'nút phân cấp' như là một lớp cơ sở cho các thực thể khác nhau. Thông thường danh sách liên kết (anh chị em, mô hình thứ 2) là cách tự nhiên mà nhiều thư viện lớp phát triển, tuy nhiên trẻ em có thể thuộc nhiều loại khác nhau và có lẽ " mô hình đối tượng " không phải là những gì chúng ta xem xét khi nói về cây nói chung.

Triển khai yêu thích của tôi về một cây (nút) của mô hình đầu tiên của bạn là một lớp lót (trong C #):

public class node : List<node> { /* props go here */ }

Kế thừa từ một Danh sách chung về loại của riêng bạn (hoặc kế thừa từ bất kỳ bộ sưu tập chung nào khác của bạn). Đi bộ có thể theo một hướng: hình thành gốc xuống (vật phẩm không biết cha mẹ của chúng).

Cây chỉ có cha mẹ

Một mô hình khác mà bạn không đề cập đến là mô hình mà mọi đứa trẻ đều có liên quan đến cha mẹ của nó:

               null
                 |
       +---------+---------------------------------+
       |       parent                              |
       | root                                      |
       +-------------------------------------------+
          |                   |                |
+---------+------+    +-------+--------+    +--+-------------+
|     parent     |    |     parent     |    |     parent     |
|     node 1     |    |     node 2     |    |     node 3     |
+----------------+    +----------------+    +----------------+

Đi bộ cây này chỉ có thể theo cách khác, thông thường tất cả các nút này sẽ được lưu trữ trong một bộ sưu tập (mảng, hashtable, dictionary, v.v.) và một nút sẽ được định vị bằng cách tìm kiếm bộ sưu tập theo các tiêu chí khác ngoài vị trí phân cấp trong cây thường không có tầm quan trọng chính.

Những cây chỉ dành cho cha mẹ thường được nhìn thấy trong các ứng dụng cơ sở dữ liệu. Thật dễ dàng để tìm thấy các phần tử con của một nút với các câu lệnh "CHỌN * WHERE ParentId = x". Tuy nhiên, chúng tôi hiếm khi tìm thấy chúng được chuyển đổi thành các đối tượng lớp nút cây như vậy. Trong các ứng dụng statefull (máy tính để bàn), chúng có thể được gói vào các điều khiển nút cây hiện có. Trong các ứng dụng phi trạng thái (web) thậm chí có thể không xảy ra. Tôi đã thấy các công cụ trình tạo lớp ánh xạ ORM đưa ra các lỗi tràn ngăn xếp khi tạo các lớp cho các bảng có mối quan hệ với chính chúng (cười khúc khích), vì vậy có lẽ những cây này không phổ biến lắm.

cây điều hướng hai chiều

Tuy nhiên, trong hầu hết các trường hợp thực tế, thật thuận tiện để có những điều tốt nhất của cả hai thế giới. Các nút có một danh sách trẻ em và ngoài ra biết cha mẹ của chúng: cây điều hướng hai chiều.

                          null
                            |
       +--------------------+--------------------+
       |                  parent                 |
       |        root                             | 
       | child1            child2         child3 |
       +--+------------------+----------------+--+
          |                  |                |
+---------+-----+    +-------+-------+    +---+-----------+
|      parent   |    |     parent    |    |  parent       |
|    node1      |    |     node2     |    |     node3     |
| child1 child2 |    | child1 child2 |    | child1 child2 |
+--+---------+--+    +--+---------+--+    +--+---------+--+
   |         |          |         |          |         |

Điều này mang lại nhiều khía cạnh hơn để xem xét:

  • Nơi để thực hiện liên kết và hủy liên kết của cha mẹ?
    • hãy để logic kinh doanh quan tâm và bỏ khía cạnh ra khỏi nút (họ sẽ quên!)
    • các nút có các phương thức để tạo trẻ em (không cho phép đặt hàng lại) (lựa chọn microsofts trong triển khai DOM System.Xml.XmlDocument của chúng, điều này gần như khiến tôi phát điên khi lần đầu tiên gặp phải nó)
    • Các nút lấy cha mẹ trong hàm tạo của chúng (không cho phép đặt hàng lại)
    • trong tất cả các phương thức add (), insert () và remove () và sự quá tải của các nút (thường là lựa chọn của tôi)
  • Kiên trì
    • Cách đi trên cây khi vẫn tồn tại (ví dụ: bỏ liên kết cha mẹ)
    • Làm cách nào để xây dựng lại liên kết hai chiều sau khi hủy tuần tự hóa (đặt lại tất cả các bậc cha mẹ thành một hành động sau khử lưu huỳnh)
  • Thông báo
    • Cơ chế tĩnh (cờ IsDenty), xử lý đệ quy trong thuộc tính?
    • Các sự kiện, bong bóng qua cha mẹ, qua trẻ em hoặc cả hai cách (ví dụ như xem xét việc bơm tin nhắn windows).

Bây giờ để trả lời câu hỏi , cây điều hướng hai chiều có xu hướng (trong sự nghiệp và lĩnh vực của tôi cho đến nay) được sử dụng rộng rãi nhất. Ví dụ là triển khai microsofts của System.Windows.Forms.Control hoặc System.Web.UI.Control trong khung .Net, nhưng mọi triển khai DOM (Mô hình đối tượng tài liệu) sẽ có các nút biết cha mẹ của chúng cũng như liệt kê của con cái họ. Lý do: dễ sử dụng hơn dễ thực hiện. Ngoài ra, đây thường là các lớp cơ sở cho các lớp cụ thể hơn (XmlNode có thể là cơ sở của các lớp Tag, Attribution và Text) và các lớp cơ sở này là nơi tự nhiên để đặt các kiến ​​trúc xử lý sự kiện và tuần tự hóa chung.

Cây nằm ở trung tâm của nhiều kiến ​​trúc, và có thể điều hướng tự do có nghĩa là có thể thực hiện các giải pháp nhanh hơn.


1

Tôi không biết bất kỳ thư viện container nào hỗ trợ trực tiếp trường hợp thứ hai của bạn, nhưng hầu hết các thư viện container đều có thể dễ dàng hỗ trợ cho kịch bản đó. Ví dụ: trong C ++, bạn có thể có:

class Node;  // forward reference to satisfy the compiler
typedef std::list<Node*> NodeList;
class Node : public NodeList { /* . . . */ };  // a node is also a list

Node* n = new Node;
n->push_back(new Node);
Node* tree = new Node;
tree->push_back(new Node);
tree->push_back(n);

Các trình phân tích cú pháp có thể sử dụng một cấu trúc tương tự như thế này, bởi vì nó hỗ trợ hiệu quả các nút với số lượng mục và con khác nhau. Tôi không biết chắc chắn vì tôi thường không đọc mã nguồn của họ.


1

Một trong những trường hợp khi có mảng trẻ em là thích hợp hơn là khi bạn cần truy cập ngẫu nhiên vào trẻ em. Và điều này thường là khi trẻ em được sắp xếp. Ví dụ, cây phân cấp giống như tệp có thể sử dụng điều này để tìm kiếm đường dẫn nhanh hơn. Hoặc cây thẻ DOM khi truy cập chỉ mục rất tự nhiên

Một ví dụ khác là khi có "con trỏ" cho tất cả trẻ em cho phép sử dụng thuận tiện hơn. Ví dụ, cả hai loại bạn mô tả có thể được sử dụng khi thực hiện quan hệ cây với cơ sở dữ liệu quan hệ. Nhưng cái trước (chi tiết tổng thể từ cha mẹ đến con cái trong trường hợp này) sẽ cho phép truy vấn bằng SQL chung cho dữ liệu hữu ích, trong khi cái sau sẽ giới hạn bạn đáng kể.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.