Thuật toán tìm kiếm đầu tiên không sâu đệ quy


173

Tôi đang tìm kiếm một thuật toán tìm kiếm đầu tiên chuyên sâu không đệ quy cho cây không nhị phân. Bất kỳ giúp đỡ được rất nhiều đánh giá cao.


1
@Bart Kiers Một cây nói chung, đánh giá bằng thẻ.
biziclop

13
Độ sâu tìm kiếm đầu tiên là một thuật toán đệ quy. Các câu trả lời dưới đây là các nút khám phá đệ quy, chúng chỉ không sử dụng ngăn xếp cuộc gọi của hệ thống để thực hiện đệ quy và thay vào đó là sử dụng một ngăn xếp rõ ràng.
Null Set

8
@Null Đặt Không, nó chỉ là một vòng lặp. Theo định nghĩa của bạn, mọi chương trình máy tính là đệ quy. (Mà, theo một nghĩa nào đó của từ này.)
biziclop

1
@Null Set: Cây cũng là cấu trúc dữ liệu đệ quy.
Gumbo

2
@MuhammadUmer lợi ích chính của phép lặp đối với các cách tiếp cận đệ quy khi phép lặp được coi là ít đọc hơn là bạn có thể tránh các hạn chế kích thước ngăn xếp / độ sâu đệ quy tối đa mà hầu hết các hệ thống / ngôn ngữ lập trình thực hiện để bảo vệ ngăn xếp. Với ngăn xếp trong bộ nhớ, ngăn xếp của bạn chỉ bị giới hạn bởi số lượng bộ nhớ mà chương trình của bạn được phép tiêu thụ, điều này thường cho phép một ngăn xếp lớn hơn nhiều so với kích thước ngăn xếp cuộc gọi tối đa.
John B

Câu trả lời:


313

DFS:

list nodes_to_visit = {root};
while( nodes_to_visit isn't empty ) {
  currentnode = nodes_to_visit.take_first();
  nodes_to_visit.prepend( currentnode.children );
  //do something
}

BFS:

list nodes_to_visit = {root};
while( nodes_to_visit isn't empty ) {
  currentnode = nodes_to_visit.take_first();
  nodes_to_visit.append( currentnode.children );
  //do something
}

Sự đối xứng của hai người khá tuyệt.

Cập nhật: Như đã chỉ ra, take_first()loại bỏ và trả về phần tử đầu tiên trong danh sách.


11
+1 để nhận thấy hai người giống nhau như thế nào khi được thực hiện không đệ quy (như thể chúng hoàn toàn khác nhau khi chúng được đệ quy, nhưng vẫn ...)
corsiKa

3
Và sau đó để thêm vào tính đối xứng, nếu bạn sử dụng hàng đợi ưu tiên tối thiểu làm rìa thay vào đó, bạn có một công cụ tìm đường dẫn ngắn nhất một nguồn.
Mark Peters

10
BTW, .first()chức năng cũng loại bỏ phần tử khỏi danh sách. Giống như shift()trong nhiều ngôn ngữ. pop()cũng hoạt động và trả về các nút con theo thứ tự từ phải sang trái thay vì từ trái sang phải.
Ariel

5
IMO, thuật toán DFS hơi không chính xác. Hãy tưởng tượng 3 đỉnh tất cả kết nối với nhau. Tiến độ nên là : gray(1st)->gray(2nd)->gray(3rd)->blacken(3rd)->blacken(2nd)->blacken(1st). Nhưng mã của bạn tạo ra : gray(1st)->gray(2nd)->gray(3rd)->blacken(2nd)->blacken(3rd)->blacken(1st).
dơi

3
@learner Tôi có thể hiểu nhầm ví dụ của bạn nhưng nếu tất cả chúng được kết nối với nhau, đó không thực sự là một cái cây.
biziclop

40

Bạn sẽ sử dụng ngăn xếp chứa các nút chưa được truy cập:

stack.push(root)
while !stack.isEmpty() do
    node = stack.pop()
    for each node.childNodes do
        stack.push(stack)
    endfor
    // …
endwhile

2
@Gumbo Tôi tự hỏi rằng nếu nó là một biểu đồ với chu kỳ. Có thể làm việc này? Tôi nghĩ rằng tôi chỉ có thể tránh để thêm nút dulplicated vào ngăn xếp và nó có thể hoạt động. Những gì tôi sẽ làm là đánh dấu tất cả các lân cận của nút được bật ra và thêm một if (nodes are not marked)để đánh giá xem nó có phù hợp để được đẩy vào ngăn xếp hay không. Có thể làm việc đó?
Alston

1
@Stallman Bạn có thể nhớ các nút mà bạn đã truy cập. Nếu sau đó bạn chỉ truy cập các nút mà bạn chưa truy cập, bạn sẽ không thực hiện bất kỳ chu kỳ nào.
Gumbo

@Gumbo Ý bạn là doing cyclesgì? Tôi nghĩ rằng tôi chỉ muốn thứ tự của DFS. Có đúng hay không, cảm ơn bạn.
Alston

Chỉ muốn chỉ ra rằng sử dụng ngăn xếp (LIFO) có nghĩa là chiều sâu đầu tiên. Thay vào đó, nếu bạn muốn sử dụng chiều rộng đầu tiên, hãy đi với một hàng đợi (FIFO).
Per Lundberg

3
Điều đáng chú ý là để có mã tương đương như câu trả lời phổ biến nhất @biziclop, bạn cần đẩy các ghi chú con theo thứ tự ngược lại ( for each node.childNodes.reverse() do stack.push(stack) endfor). Đây cũng có thể là những gì bạn muốn. Lời giải thích tuyệt vời tại sao nó lại như vậy trong video này: youtube.com/watch?v=cZPXfl_tUkA endfor
Mariusz Pawelski

32

Nếu bạn có con trỏ tới các nút cha, bạn có thể làm điều đó mà không cần thêm bộ nhớ.

def dfs(root):
    node = root
    while True:
        visit(node)
        if node.first_child:
            node = node.first_child      # walk down
        else:
            while not node.next_sibling:
                if node is root:
                    return
                node = node.parent       # walk up ...
            node = node.next_sibling     # ... and right

Lưu ý rằng nếu các nút con được lưu trữ dưới dạng một mảng thay vì thông qua các con trỏ anh chị em, thì anh chị em tiếp theo có thể được tìm thấy như sau:

def next_sibling(node):
    try:
        i =    node.parent.child_nodes.index(node)
        return node.parent.child_nodes[i+1]
    except (IndexError, AttributeError):
        return None

Đây là một giải pháp tốt vì nó không sử dụng bộ nhớ bổ sung hoặc thao tác danh sách hoặc ngăn xếp (một số lý do chính đáng để tránh đệ quy). Tuy nhiên, chỉ có thể nếu các nút cây có liên kết đến cha mẹ của chúng.
joeytwiddle

Cảm ơn bạn. Thuật toán này là tuyệt vời. Nhưng trong phiên bản này, bạn không thể xóa bộ nhớ của nút trong chức năng truy cập. Thuật toán này có thể chuyển đổi cây thành danh sách liên kết đơn bằng cách sử dụng con trỏ "first_child". Hơn bạn có thể đi qua nó và bộ nhớ của nút miễn phí mà không cần đệ quy.
puchu

6
"Nếu bạn có con trỏ tới các nút cha, bạn có thể làm điều đó mà không cần bộ nhớ bổ sung": lưu trữ con trỏ đến các nút cha sẽ sử dụng một số "bộ nhớ bổ sung" ...
rptr

1
@ rptr87 nếu nó không rõ ràng, không có bộ nhớ bổ sung ngoài các con trỏ đó.
Abhinav Gauniyal

Điều này sẽ thất bại đối với các cây một phần nơi nút không phải là gốc tuyệt đối, nhưng có thể dễ dàng sửa bằng while not node.next_sibling or node is root:.
Basel Shishani

5

Sử dụng ngăn xếp để theo dõi các nút của bạn

Stack<Node> s;

s.prepend(tree.head);

while(!s.empty) {
    Node n = s.poll_front // gets first node

    // do something with q?

    for each child of n: s.prepend(child)

}

1
@Dave O. Không, bởi vì bạn đẩy lùi những đứa trẻ của nút được truy cập trước tất cả mọi thứ đã có.
biziclop

Tôi đã hiểu sai ngữ nghĩa của Push_back rồi.
Dave O.

@Dave bạn có một điểm rất tốt. Tôi đã nghĩ rằng nó nên "đẩy phần còn lại của hàng đợi" chứ không phải "đẩy về phía sau". Tôi sẽ chỉnh sửa một cách thích hợp.
corsiKa

Nếu bạn đang đẩy ra phía trước, nó sẽ là một chồng.
chuyến bay

@Timmy yeah Tôi không chắc tôi đã nghĩ gì ở đó. @quasiverse Chúng ta thường nghĩ về một hàng đợi như một hàng đợi FIFO. Một ngăn xếp được định nghĩa là một hàng đợi LIFO.
corsiKa

4

Mặc dù "sử dụng một ngăn xếp" có thể hoạt động như câu trả lời cho câu hỏi phỏng vấn, nhưng thực tế, nó chỉ thực hiện rõ ràng những gì một chương trình đệ quy làm đằng sau hậu trường.

Đệ quy sử dụng ngăn xếp chương trình tích hợp. Khi bạn gọi một hàm, nó sẽ đẩy các đối số đến hàm lên ngăn xếp và khi hàm trả về, nó sẽ làm như vậy bằng cách bật ngăn xếp chương trình.


7
Với sự khác biệt quan trọng là ngăn xếp luồng bị hạn chế nghiêm trọng và thuật toán không đệ quy sẽ sử dụng heap có thể mở rộng hơn nhiều.
Yam Marcovic

1
Đây không chỉ là một tình huống giả định. Tôi đã sử dụng các kỹ thuật như thế này trong một số trường hợp trong C # và JavaScript để đạt được hiệu suất đáng kể so với các bộ chuyển đổi cuộc gọi đệ quy hiện có. Nó thường là trường hợp quản lý đệ quy với một ngăn xếp thay vì sử dụng ngăn xếp cuộc gọi nhanh hơn và ít tốn tài nguyên hơn. Có rất nhiều chi phí liên quan đến việc đặt bối cảnh cuộc gọi lên ngăn xếp so với lập trình viên có thể đưa ra quyết định thực tế về những gì sẽ đặt trên ngăn xếp tùy chỉnh.
Jason Jackson

4

Một triển khai ES6 dựa trên câu trả lời tuyệt vời của biziclops:

root = {
  text: "root",
  children: [{
    text: "c1",
    children: [{
      text: "c11"
    }, {
      text: "c12"
    }]
  }, {
    text: "c2",
    children: [{
      text: "c21"
    }, {
      text: "c22"
    }]
  }, ]
}

console.log("DFS:")
DFS(root, node => node.children, node => console.log(node.text));

console.log("BFS:")
BFS(root, node => node.children, node => console.log(node.text));

function BFS(root, getChildren, visit) {
  let nodesToVisit = [root];
  while (nodesToVisit.length > 0) {
    const currentNode = nodesToVisit.shift();
    nodesToVisit = [
      ...nodesToVisit,
      ...(getChildren(currentNode) || []),
    ];
    visit(currentNode);
  }
}

function DFS(root, getChildren, visit) {
  let nodesToVisit = [root];
  while (nodesToVisit.length > 0) {
    const currentNode = nodesToVisit.shift();
    nodesToVisit = [
      ...(getChildren(currentNode) || []),
      ...nodesToVisit,
    ];
    visit(currentNode);
  }
}


3
PreOrderTraversal is same as DFS in binary tree. You can do the same recursion 
taking care of Stack as below.

    public void IterativePreOrder(Tree root)
            {
                if (root == null)
                    return;
                Stack s<Tree> = new Stack<Tree>();
                s.Push(root);
                while (s.Count != 0)
                {
                    Tree b = s.Pop();
                    Console.Write(b.Data + " ");
                    if (b.Right != null)
                        s.Push(b.Right);
                    if (b.Left != null)
                        s.Push(b.Left);

                }
            }

Logic chung là, đẩy một nút (bắt đầu từ gốc) vào giá trị Stack, Pop () và Print (). Sau đó, nếu nó có con (trái và phải) đẩy chúng vào ngăn xếp - đẩy Phải trước để bạn sẽ đến thăm con trái trước (sau khi truy cập nút chính). Khi ngăn xếp trống () bạn sẽ truy cập tất cả các nút trong Đơn đặt hàng trước.


2

DFS không đệ quy sử dụng bộ tạo ES6

class Node {
  constructor(name, childNodes) {
    this.name = name;
    this.childNodes = childNodes;
    this.visited = false;
  }
}

function *dfs(s) {
  let stack = [];
  stack.push(s);
  stackLoop: while (stack.length) {
    let u = stack[stack.length - 1]; // peek
    if (!u.visited) {
      u.visited = true; // grey - visited
      yield u;
    }

    for (let v of u.childNodes) {
      if (!v.visited) {
        stack.push(v);
        continue stackLoop;
      }
    }

    stack.pop(); // black - all reachable descendants were processed 
  }    
}

Nó đi chệch khỏi DFS không đệ quy điển hình để dễ dàng phát hiện khi tất cả các hậu duệ có thể tiếp cận của nút đã cho được xử lý và để duy trì đường dẫn hiện tại trong danh sách / ngăn xếp.


1

Giả sử bạn muốn thực thi một thông báo khi mỗi nút trong biểu đồ được truy cập. Việc thực hiện đệ quy đơn giản là:

void DFSRecursive(Node n, Set<Node> visited) {
  visited.add(n);
  for (Node x : neighbors_of(n)) {  // iterate over all neighbors
    if (!visited.contains(x)) {
      DFSRecursive(x, visited);
    }
  }
  OnVisit(n);  // callback to say node is finally visited, after all its non-visited neighbors
}

Ok, bây giờ bạn muốn triển khai dựa trên ngăn xếp vì ví dụ của bạn không hoạt động. Ví dụ, các biểu đồ phức tạp có thể khiến điều này làm nổ tung chương trình của bạn và bạn cần triển khai một phiên bản không đệ quy. Vấn đề lớn nhất là biết khi nào nên đưa ra thông báo.

Các mã giả sau đây hoạt động (kết hợp Java và C ++ để dễ đọc):

void DFS(Node root) {
  Set<Node> visited;
  Set<Node> toNotify;  // nodes we want to notify

  Stack<Node> stack;
  stack.add(root);
  toNotify.add(root);  // we won't pop nodes from this until DFS is done
  while (!stack.empty()) {
    Node current = stack.pop();
    visited.add(current);
    for (Node x : neighbors_of(current)) {
      if (!visited.contains(x)) {
        stack.add(x);
        toNotify.add(x);
      }
    }
  }
  // Now issue notifications. toNotifyStack might contain duplicates (will never
  // happen in a tree but easily happens in a graph)
  Set<Node> notified;
  while (!toNotify.empty()) {
  Node n = toNotify.pop();
  if (!toNotify.contains(n)) {
    OnVisit(n);  // issue callback
    toNotify.add(n);
  }
}

Trông có vẻ phức tạp nhưng logic bổ sung cần thiết để phát hành thông báo tồn tại bởi vì bạn cần thông báo theo thứ tự truy cập ngược - DFS bắt đầu từ root nhưng thông báo lần cuối, không giống như BFS rất đơn giản để thực hiện.

Đối với các cú đá, hãy thử biểu đồ sau: các nút là s, t, v và w. các cạnh được định hướng là: s-> t, s-> v, t-> w, v-> w và v-> t. Chạy triển khai DFS của riêng bạn và thứ tự các nút nên được truy cập phải là: w, t, v, s Việc triển khai DFS vụng về có thể thông báo cho t trước và điều đó cho thấy có lỗi. Việc triển khai đệ quy DFS sẽ luôn đạt đến w cuối cùng.


1

Ví dụ đầy đủ Mã làm việc, không có ngăn xếp:

import java.util.*;

class Graph {
private List<List<Integer>> adj;

Graph(int numOfVertices) {
    this.adj = new ArrayList<>();
    for (int i = 0; i < numOfVertices; ++i)
        adj.add(i, new ArrayList<>());
}

void addEdge(int v, int w) {
    adj.get(v).add(w); // Add w to v's list.
}

void DFS(int v) {
    int nodesToVisitIndex = 0;
    List<Integer> nodesToVisit = new ArrayList<>();
    nodesToVisit.add(v);
    while (nodesToVisitIndex < nodesToVisit.size()) {
        Integer nextChild= nodesToVisit.get(nodesToVisitIndex++);// get the node and mark it as visited node by inc the index over the element.
        for (Integer s : adj.get(nextChild)) {
            if (!nodesToVisit.contains(s)) {
                nodesToVisit.add(nodesToVisitIndex, s);// add the node to the HEAD of the unvisited nodes list.
            }
        }
        System.out.println(nextChild);
    }
}

void BFS(int v) {
    int nodesToVisitIndex = 0;
    List<Integer> nodesToVisit = new ArrayList<>();
    nodesToVisit.add(v);
    while (nodesToVisitIndex < nodesToVisit.size()) {
        Integer nextChild= nodesToVisit.get(nodesToVisitIndex++);// get the node and mark it as visited node by inc the index over the element.
        for (Integer s : adj.get(nextChild)) {
            if (!nodesToVisit.contains(s)) {
                nodesToVisit.add(s);// add the node to the END of the unvisited node list.
            }
        }
        System.out.println(nextChild);
    }
}

public static void main(String args[]) {
    Graph g = new Graph(5);

    g.addEdge(0, 1);
    g.addEdge(0, 2);
    g.addEdge(1, 2);
    g.addEdge(2, 0);
    g.addEdge(2, 3);
    g.addEdge(3, 3);
    g.addEdge(3, 1);
    g.addEdge(3, 4);

    System.out.println("Breadth First Traversal- starting from vertex 2:");
    g.BFS(2);
    System.out.println("Depth First Traversal- starting from vertex 2:");
    g.DFS(2);
}}

đầu ra: Breadth First Traversal- bắt đầu từ đỉnh 2: 2 0 3 1 4 Độ sâu Traversal First- bắt đầu từ đỉnh 2: 2 3 4 1 0


0

Bạn có thể sử dụng một ngăn xếp. Tôi đã triển khai các biểu đồ với Ma trận điều chỉnh:

void DFS(int current){
    for(int i=1; i<N; i++) visit_table[i]=false;
    myStack.push(current);
    cout << current << "  ";
    while(!myStack.empty()){
        current = myStack.top();
        for(int i=0; i<N; i++){
            if(AdjMatrix[current][i] == 1){
                if(visit_table[i] == false){ 
                    myStack.push(i);
                    visit_table[i] = true;
                    cout << i << "  ";
                }
                break;
            }
            else if(!myStack.empty())
                myStack.pop();
        }
    }
}

0

Lặp lại DFS trong Java:

//DFS: Iterative
private Boolean DFSIterative(Node root, int target) {
    if (root == null)
        return false;
    Stack<Node> _stack = new Stack<Node>();
    _stack.push(root);
    while (_stack.size() > 0) {
        Node temp = _stack.peek();
        if (temp.data == target)
            return true;
        if (temp.left != null)
            _stack.push(temp.left);
        else if (temp.right != null)
            _stack.push(temp.right);
        else
            _stack.pop();
    }
    return false;
}

Câu hỏi yêu cầu rõ ràng cho một cây không nhị phân
user3743222

Bạn cần một bản đồ được truy cập để tránh vòng lặp vô hạn
xoắn ốc

0

http://www.youtube.com/watch?v=zLZhSSXAwxI

Chỉ cần xem video này và đi ra với thực hiện. Có vẻ dễ hiểu cho tôi. Hãy phê bình điều này.

visited_node={root}
stack.push(root)
while(!stack.empty){
  unvisited_node = get_unvisited_adj_nodes(stack.top());
  If (unvisited_node!=null){
     stack.push(unvisited_node);  
     visited_node+=unvisited_node;
  }
  else
     stack.pop()
}

0

Sử dụng Stack, đây là các bước để làm theo: Đẩy đỉnh đầu tiên trên ngăn xếp sau đó,

  1. Nếu có thể, hãy truy cập một đỉnh không mong muốn liền kề, đánh dấu nó và đẩy nó lên ngăn xếp.
  2. Nếu bạn không thể làm theo bước 1, thì nếu có thể, hãy bật một đỉnh khỏi ngăn xếp.
  3. Nếu bạn không thể làm theo bước 1 hoặc bước 2, bạn đã hoàn thành.

Đây là chương trình Java theo các bước trên:

public void searchDepthFirst() {
    // begin at vertex 0
    vertexList[0].wasVisited = true;
    displayVertex(0);
    stack.push(0);
    while (!stack.isEmpty()) {
        int adjacentVertex = getAdjacentUnvisitedVertex(stack.peek());
        // if no such vertex
        if (adjacentVertex == -1) {
            stack.pop();
        } else {
            vertexList[adjacentVertex].wasVisited = true;
            // Do something
            stack.push(adjacentVertex);
        }
    }
    // stack is empty, so we're done, reset flags
    for (int j = 0; j < nVerts; j++)
            vertexList[j].wasVisited = false;
}

0
        Stack<Node> stack = new Stack<>();
        stack.add(root);
        while (!stack.isEmpty()) {
            Node node = stack.pop();
            System.out.print(node.getData() + " ");

            Node right = node.getRight();
            if (right != null) {
                stack.push(right);
            }

            Node left = node.getLeft();
            if (left != null) {
                stack.push(left);
            }
        }

0

Mã giả dựa trên câu trả lời của @ biziclop:

  • Chỉ sử dụng các cấu trúc cơ bản: biến, mảng, nếu, trong khi và cho
  • Chức năng getNode(id)getChildren(id)
  • Giả sử số nút đã biết N

LƯU Ý: Tôi sử dụng lập chỉ mục mảng từ 1, không phải 0.

Bề rộng đầu tiên

S = Array(N)
S[1] = 1; // root id
cur = 1;
last = 1
while cur <= last
    id = S[cur]
    node = getNode(id)
    children = getChildren(id)

    n = length(children)
    for i = 1..n
        S[ last+i ] = children[i]
    end
    last = last+n
    cur = cur+1

    visit(node)
end

Chiều sâu trước

S = Array(N)
S[1] = 1; // root id
cur = 1;
while cur > 0
    id = S[cur]
    node = getNode(id)
    children = getChildren(id)

    n = length(children)
    for i = 1..n
        // assuming children are given left-to-right
        S[ cur+i-1 ] = children[ n-i+1 ] 

        // otherwise
        // S[ cur+i-1 ] = children[i] 
    end
    cur = cur+n-1

    visit(node)
end

0

Đây là một liên kết đến một chương trình java hiển thị DFS theo cả hai phương pháp reccursive và không reccursive và cũng tính toán thời gian khám phákết thúc , nhưng không có laleling cạnh.

    public void DFSIterative() {
    Reset();
    Stack<Vertex> s = new Stack<>();
    for (Vertex v : vertices.values()) {
        if (!v.visited) {
            v.d = ++time;
            v.visited = true;
            s.push(v);
            while (!s.isEmpty()) {
                Vertex u = s.peek();
                s.pop();
                boolean bFinished = true;
                for (Vertex w : u.adj) {
                    if (!w.visited) {
                        w.visited = true;
                        w.d = ++time;
                        w.p = u;
                        s.push(w);
                        bFinished = false;
                        break;
                    }
                }
                if (bFinished) {
                    u.f = ++time;
                    if (u.p != null)
                        s.push(u.p);
                }
            }
        }
    }
}

Nguồn đầy đủ ở đây .


0

Chỉ muốn thêm thực hiện python của tôi vào danh sách dài các giải pháp. Thuật toán không đệ quy này đã phát hiện và kết thúc các sự kiện.


worklist = [root_node]
visited = set()
while worklist:
    node = worklist[-1]
    if node in visited:
        # Node is finished
        worklist.pop()
    else:
        # Node is discovered
        visited.add(node)
        for child in node.children:
            worklist.append(child)
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.