Basic DFS space usage - python

I have a basic question related to space usage, and I'm using DFS as an example. I'm not sure if the space usage in these few implementations are the same, or if a few actually differ. My interpretation of space usage is directly correlated to what the function allocates. Can anyone help me verify the space usage of these few examples I made? This is a question on space complexity, and not time + functionality
Example 1: We allocate a dictionary that will store N nodes. I'm positive this one allocates O(N) space.
class Node:
def __init__(self, children):
self.children = children
def getChildren(self):
return self.children
def dfs(start):
stack = []
visited = {}
stack.append(start)
while(len(stack) > 0):
node = stack.pop()
if(node not in visited):
visited[node] = True
for child in node.getChildren():
stack.append(child)
Example 2: We don't allocate anything in the dfs function, but instead we are given a flag to set on the Node. We aren't allocating anything in the dfs function so it is O(1) space usage.
class Node:
def __init__(self, children):
self.children = children
self.visited = False
def getChildren(self):
return self.children
def getVisited(self):
return self.visited
def setVisited(self, visit):
self.visited = visit
def dfs(start):
stack = []
stack.append(start)
while(len(stack) > 0):
node = stack.pop()
if(!node.getVisited()):
node.setVisited(True)
for child in node.getChildren():
stack.append(child)
Example 3: We have an object Node that can be manipulated, but does not have a flag attribute up front. DFS is manually creating a flag on each Node, and thus allocating O(N) space.
class Node:
def __init__(self, children):
self.children = children
def getChildren(self):
return self.children
def dfs(start):
stack = []
stack.append(start)
while(len(stack) > 0):
node = stack.pop()
if(node.visited is not None):
node.visited = True
for child in node.getChildren():
stack.append(child)

Space complexity is not determined by where the space gets allocated but by how much space (memory) is required to hold a given data structure in relation to the number of objects to be processed by an algorithm.
In your examples all data structures require O(N) space (N = # of nodes)

Related

BFS / level order traversal in Non-Binary Tree using queue

I have to make two classes: NonBinaryTree and SingleNode class containig some methods working on nodes as well as entire tree (in class NonBinaryTree). I have encountered problems with implementing BFS (level order) traversal through Non Binary Tree using queue (first in, first out type). As there are many resources for Binary Tree, where each node have up to two children, I have not found anything that could help me solve problem with Non Binary Tree.
So far, I made this code:
import queue
from typing import List, Callable
class SingleNode:
def __init__(self, name : str):
self.name : str = name
self.children : List['SingleNode'] = []
def add(self, *nodes : List['SingleNode']):
for node in nodes:
self.children.append(node)
def is_leaf(self):
if len(self.children) == 0:
return True
return False
def level_order_traversal(self, visit: Callable[['SingleNode'], None]) -> List[List[int]]:
fifo = queue.Queue()
levels = []
fifo.put([root])
while fifo and root:
currNode, nextLevel = [], []
while not fifo.empty():
node = fifo.get()
currNode.append(node)
for child in node.children:
nextLevel.append(child)
fifo.put(nextLevel)
levels.append(currNode)
return levels
def search(self, name : str):
if self.name == name:
print(self.__repr__())
for child in self.children:
child.search(name)
return None
def __str__(self):
return f"{self.name}"
def __repr__(self):
return f"TreeNode({self.name}) : {self.children}"
class NonBinaryTree:
root_node: SingleNode
My tree:
enter image description here
I need to go on nodes in this order: 1, 2, 3, 4, 5, 6, 7, 8, and so on...
Why don't you follow similar approach as BFS traversal in binary tree, it's just in this case it's non binary but the logic is always gonna be same,
class Solution:
def levelOrder(self, root: 'Node') -> List[List[int]]:
levels = []
queue = [root]
while queue and root:
currNode,nextLevel = [],[]
for node in queue:
currNode.append(node.val)
for child in node.children:
nextLevel.append(child)
queue = nextLevel
levels.append(currNode)
return levels

How does Python represent the LinkedList in terms of memory reference?

Note: I prefer to not use any external module since it is for interview prep purpose.
I know that there is no a built-in linked-list DS in python. However, we can implement the linked-list through a class Node. In the following code, I did a method (intersect_ll) to find an intersection between two linkedlists where the definition of intersections: node(s) are in the same order and value in the both linkedlists:
class Node:
def __init__(self, data=None, next=None):
self.data = data
self.next = next
class SingleLinkedList:
def __init__(self):
self.head = None
def add_node(self, data):
newNode = Node(data)
if self.head:
current = self.head
while current.next:
current = current.next
current.next = newNode
else:
self.head = newNode
def print_ll(self):
current = self.head
ll_data = []
while current:
ll_data += [current.data]
current = current.next
return ll_data
def intesect_ll (self, first_ll, second_ll):
current_first = first_ll.head
current_second = second_ll.head
if current_first is None or current_second is None:
return False
list_intersect = []
while current_first and current_second:
if current_first.data == current_second.data:
list_intersect += [current_first.data]
current_first = current_first.next
current_second = current_second.next
for item in list_intersect:
self.add_node(item)
return self.print_ll()
My Question is: :
I am pretty new to python so I am struggling to understand why comparing instead by memory reference is not working. In other word, why python did not give these two nodes with the same value and order in both linkedlists, the same memory location ?!. Is that because I am implementing my own data-structure and hence,I assume that python would take care of the rest which is not the reality?
if current_first is current_second
compiler result:
it gives two different memory references for the both nodes of the same value and order in both linkedlists. Hence, this does not work and need to make it (.data) comparing by value then.
I found out that what I expect from python to do in terms of memory management is not that what really happens. I need to allocate the same object and then reference it in both linked lists in order to comparing by reference becomes a valid solution.
assign ObjectA : locationA in memory
linkedlist1: node.next --> LocationA ---> n nodes
linkedlist2: node.next --> LocationA ----> k nodes
Otherwise, they are completely two different linkedlists and the interpreter would assign each one of the nodes into different memory location:
linkedlist1: node.next ---> locationX -- n nodes
linkedlist2: node.next ---> locationY --- k nodes
Hope that helps anyone would have the same concern

Get all nodes on a given level in a binary tree with python

i am trying to get a list of nodes (objetcs) in a python binary tree, i am looking for a recursive function implemented in the node object, so i will call function on the root node, and it will going down on childs nodes till reachs the specific level, and then will return those nodes in a list
My current aproach, i'm not sure if this is correct or the best way to implement it:
def get_level_nodes(self, nodes, level=1):
if self.level > level:
return nodes
if self.level == level:
nodes.append(self)
return nodes
for child in self.child_id:
nodes += child.get_level_nodes(node, level)
return nodes
# Getting the list
nodes_list = root_node.get_level_nodes([], 3)
There is no real need to pass a list of nodes around. Each node can just return the appropriate level-nodes of its own subtree, and leave the combining of neighbours to the parent:
def get_level_nodes(self, level=1):
if self.level > level:
return []
if self.level == level:
return [self]
# child_id seems an odd name
return [n for c in self.children for n in c.get_level_nodes(level)]
A more space-efficient implementation that does not build intermediate lists for each subtree would be a generator function:
def get_level_nodes(self, level=1):
if self.level > level:
return
if self.level == level:
yield self
else:
for c in self.children:
for n in c.get_level_nodes(level):
yield n
# or in Python3
# yield from c.get_level_nodes(level)
nodes_list = list(root_node.get_level_nodes(3))

Implementing a Depth First Traversal based on a strategy in Python

Good day.
I have a problem implementing a depth first search based on a Strategy, which is defined in a strategy.py class. There is also a graph and a traversal class. The traversal class is responsible for well, traversing the graph.
The strategy class is as follows:
class Strategy:
init_priority = 0
def __init__(self, init_pri = 0):
self.init_priority = init_pri
def init(self, graph, node):
"""Called at beginning of traversal process. Expected that
this will carry out any necessary initialisation for the
specific traversal process
"""
pass
def visit(self, node, pri):
"""Called whenever NODE is visited by a traversal process.
PRI is the priority associated with the node in the priority
queue used by the traversal process.
"""
pass
def complete(self, node):
"""Called at the end of all the processing performed in visiting NODE.
"""
pass
def discover(self, nbr, node, weight, pri):
"""Return the priority that should be associated with NBR when it is
added to the priority queue.
Called whenever NBR is discovered for the first time. NODE
is the node from which the neighbour was discovered, and
WEIGHT is the value on the edge from NODE to NBR. PRI is the
value associated with NODE in the priority queue, at the time
of discovering NBR.
"""
def rediscover(self, nbr, node, weight, pri):
"""Return the priority that should be associated with NBR when it is
added to the priority queue.
Called whenever NBR is rediscovered. NODE is the node from
which the neighbour is rediscovered, and WEIGHT is the value
associated with the edge from NODE to NBR. PRI is the
priority of NODE in the priority queue. It is provided in
case it is relevant to the traversal strategy (e.g. for Dijkstra's)
"""
pass
def getResult(self):
"""Called at the end of the traversal process. It should
return whatever is relevant or appropriate for the type of
traversal implemented by this strategy.
"""
pass
I managed to implement a breadth first search as follows:
class BreadthFirst(Strategy):
sequence = None # the sequence in which nodes are visted
treeEdges = None # the edges used to visit the nodes traversed
root = -1 # the origin of the traversal
last_pri = -1 # the most recent priority used
def __init__(self):
"""The BreadthFirst strategy uses an initial priority of 0"""
Strategy(0)
def init(self, graph, node):
"""We reset all our state information so that old traversals do not
affect the one that is about to start."""
self.last_pri = self.init_priority
self.treeEdges = []
self.sequence = []
self.root = -1
def visit(self, node, src, pri):
"""Breadth first traversal pays no attention to weights."""
self.sequence.append(node)
if src == -1:
self.root = node
else:
self.treeEdges.append((src, node))
def complete(self, node):
pass
def discover(self, nbr, node, pri):
"""Want FIFO behaviour so increment priority (ignore weights)"""
self.last_pri += 1
return self.last_pri
def rediscover(self, nbr, node, pri):
"""Rules for rediscovery same as for discovery (because weights are
ignored)"""
self.last_pri += 1
return self.last_pri
def getResult(self):
"""Return the details of the traversal as a dictionary."""
return {"origin":self.root,
"tree":self.treeEdges,
"sequence":self.sequence}
Depth first is giving me a hassle of a time though. Here's what I have so far:
class DepthFirst(Strategy):
forward = None # the forward sequence in which nodes are visted
back = None # the backward sequence in which nodes are visited
treeEdges = None # the edges used to visit the nodes traversed
cross = None
root = -1 # the origin of the traversal
last_pri = -1 # the most recent priority used
def __init__(self):
"""The DepthFirst strategy uses an initial priority of 0"""
Strategy(0)
def init(self, graph, node):
"""Called at beginning of traversal process. Expected that
this will carry out any necessary initialisation for the
specific traversal process
"""
self.last_pri = self.init_priority
self.treeEdges = []
self.forward = []
self.back = []
self.cross = []
def visit(self, node, src, pri):
"""Called whenever NODE is visited by a traversal process.
PRI is the priority associated with the node in the priority
queue used by the traversal process.
"""
self.forward.append(node)
if src == -1:
self.root = node
else:
self.treeEdges.append((src, node))
def complete(self, node):
"""Called at the end of all the processing performed in visiting NODE.
"""
if node not in self.forward:
self.cross.append(node)
def discover(self, nbr, node, pri):
"""Return the priority that should be associated with NBR when it is
added to the priority queue.
Called whenever NBR is discovered for the first time. NODE
is the node from which the neighbour was discovered, and
WEIGHT is the value on the edge from NODE to NBR. PRI is the
value associated with NODE in the priority queue, at the time
of discovering NBR.
"""
self.forward.append((node, nbr))
self.last_pri -= 1
return self.last_pri
def rediscover(self, nbr, node, pri):
"""Return the priority that should be associated with NBR when it is
added to the priority queue.
Called whenever NBR is rediscovered. NODE is the node from
which the neighbour is rediscovered, and WEIGHT is the value
associated with the edge from NODE to NBR. PRI is the
priority of NODE in the priority queue. It is provided in
case it is relevant to the traversal strategy (e.g. for Dijkstra's)
"""
self.back.append((nbr, node))
self.last_pri -= 1
return self.last_pri
def getResult(self):
"""Called at the end of the traversal process. It should
return whatever is relevant or appropriate for the type of
traversal implemented by this strategy.
"""
return {"tree":self.treeEdges,
"forward":self.forward,
"back":self.back,
"cross":self.cross}
Any tips, pointers? They would be well appreciated.
if you were just writing the two, you'd do the usual iterative loop, using a stack for DFS and a queue for BFS. here you are unifying those with a priority queue. so you need to make the priorities up so that those two behaviours come out. for DFS that means that every time you add something it has higher priority than before (so it comes out before what's already in there) - an increasing positive number is fine. for BFS it needs to be lower than anything you have added so far (so it comes out after what's already in there) - a decreasing negative number works well.
this is just my take from scanning your code. i may be wrong and i'm not going to look in detail - i just thought it was an interesting way of looking at things that might help.
ps it's normal to tag homework with "homework". if you don't, people will bitch.

How to use a generator to iterate over a tree's leafs

The problem:
I have a trie and I want to return the information stored in it. Some leaves have information (set as value > 0) and some leaves do not. I would like to return only those leaves that have a value.
As in all trie's number of leaves on each node is variable, and the key to each value is actually made up of the path necessary to reach each leaf.
I am trying to use a generator to traverse the tree postorder, but I cannot get it to work. What am I doing wrong?
My module:
class Node():
'''Each leaf in the trie is a Node() class'''
def __init__(self):
self.children = {}
self.value = 0
class Trie():
'''The Trie() holds all nodes and can return a list of their values'''
def __init__(self):
self.root = Node()
def add(self, key, value):
'''Store a "value" in a position "key"'''
node = self.root
for digit in key:
number = digit
if number not in node.children:
node.children[number] = Node()
node = node.children[number]
node.value = value
def __iter__(self):
return self.postorder(self.root)
def postorder(self, node):
if node:
for child in node.children.values():
self.postorder(child)
# Do my printing / job related stuff here
if node.value > 0:
yield node.value
Example use:
>>trie = Trie()
>>trie.add('foo', 3)
>>trie.add('foobar', 5)
>>trie.add('fobaz', 23)
>>for key in trie:
>>....print key
>>
3
5
23
I know that the example given is simple and can be solved using any other data structure. However, it is important for this program to use a trie as it is very beneficial for the data access patterns.
Thanks for the help!
Note: I have omitted newlines in the code block to be able to copy-paste with greater ease.
Change
self.postorder(child)
to
for n in self.postorder(child):
yield n
seems to make it work.
P.S. It is very helpful for you to left out the blank lines for ease of cut & paste :)

Categories

Resources