Python Algorithms Learning Notes(2)—Implementing Graphs and Trees

It’s been a week since last time I read Python Algorithms ,time flies. There are always so many errands keeping me from reading a tech book constantly.
I think keeping notes in blog helps drive me to continue .
One thing bothers me though ,I’m not sure about the language mistakes I made in blogs ,which I believe must be a lot.
I dont’t know how to check out all my mistakes to improve.
And I might end up writing my own ‘freestyle’ English ,without any real progress.


Anyway,today I’m reading about Graphs anf Trees.


1.Graphs

1.1 Basic Concepts

Graphs are one of the most powerful frameworks in algorithms.
Graphs can represent all kinds of strctures and systems we may encounter solving algorithmic problems.

A graph G=(V,E) consists of a set of nodes ,V, and edges E between them.
If the edges have a direction ,we say the graph is directed.
If we associate a weight with each edge in G , we say G is a weighted graph

A forest is a cycle-free graph, a connected graph is a tree

BLACK BOX
Hashing is used in the machanism of dictionaries and sets
Accessing elements of a dict or set can be assumed to take constant time ,basically,Θ(1) in average

1.2 Implementing of graphs

(1)Adjacency lists and the like

Basically, for each node,
we can access a list (or set or other container or iterable) of its neighbors。

Let’s take the simplest way of implementing this, assuming we have n nodes, numbered 0…n–1.
GraphSample

Method1. A straightforward Adjaceny Set Representation

a,b,c,d,e,f,g,h = range(5)
N = [
{b, c, d, e, f}, # a
{c, e},          # b
{d},             # c
{e},             # d
{f},             # e
{c, g, h},       # f
{f, h},          # g
{f, g}           # h
]      

N[v] is now a set of v’s neighbors.

>>>b in N[a]    # Neighborhood membership
True
>>> len(N[f])    # Degree
3

Method 2. Adjacency Lists

a, b, c, d, e, f, g, h = range(8)
N = [
[b, c, d, e, f],      # a
[c, e],               # b
[d],                  # c
[e],                  # d
[f],                  # e
[c, g, h],            # f
[f, h],               # g
[f, g]                # h
]

Replace the adjacency sets with adjacency lists.
The membership checking is now Θ(n) ,which is a significant slow down.
But it’s useful and neccessary if we need to iterate over neighbors and the like.

Method3.Adjacency dicts with Edge Weights

a, b, c, d, e, f, g, h = range(8)
N = [
{b:2, c:1, d:3, e:9, f:4},   # a
{c:4, e:3},                  # b
{d:8},                       # c
{e:7},                       # d
{f:5},                       # e
{c:2, g:2, h:2},             # f
{f:1, h:6},                  # g
{f:9, g:8}                   # h
]

The adjacency dict version can be used just like the others, with the additional edge weight functionality :

>>> b in N[a]   # Neighborhood membership
True
>>> len(N[f])   # Degree
3
>>> N[a][b]   # Edge weight for (a, b)
2

Method4.A Dict with Adjacency Sets
A more flexible approach (allowing us to use arbitrary,hashable, node labels) is to use a dict as this main structure.

N = {
'a': set('bcdef'),
'b': set('ce'),
'c': set('d'),
'd': set('e'),
'e': set('f'),
'f': set('cgh'),
'g': set('fh'),
'h': set('fg')
}

Note that nodes are now represented by characters.

(2)Adjacency Matrices

Instead of listing all neighbors for each node, we have one row (an array) with one position for each possible neighbor (that is, one for each node in the graph), and store a value (such as True or False),indicating whether that node is indeed a neighbor.

Method1.An Adjacency Matrix, Implemented with Nested Lists

a, b, c, d, e, f, g, h = range(8)
# a b c d e f g h
N = [[0,1,1,1,1,1,0,0], # a
[0,0,1,0,1,0,0,0], # b
[0,0,0,1,0,0,0,0], # c
[0,0,0,0,1,0,0,0], # d
[0,0,0,0,0,1,0,0], # e
[0,0,1,0,0,0,1,1], # f
[0,0,0,0,0,1,0,1], # g
[0,0,0,0,0,1,1,0]] # h

The way we’d use this is slightly different from the adjacency lists/sets.

>>> N[a][b] # Neighborhood membership
1
>>> sum(N[f]) # Degree
3

Method2.A Weight Matrix with Infinite Weight for Missing Edges

a, b, c, d, e, f, g, h = range(8)
_ = float('inf')
# a b c d e f g h
W = [[0,2,1,3,9,4,_,_], # a
[_,0,4,_,3,_,_,_], # b
[_,_,0,8,_,_,_,_], # c
[_,_,_,0,7,_,_,_], # d
[_,_,_,_,0,5,_,_], # e
[_,_,2,_,_,0,2,2], # f
[_,_,_,_,_,1,0,6], # g
[_,_,_,_,_,9,8,0]] # h

Weight matrices make it easy to access edge weights, of course, but membership checking and finding the degree of a node, for example, or even iterating over neighbors must be done a bit differently now. You need to take the infinity value into account–for example, like this (using inf = float(‘inf’) for more readable code)

>>> W[a][b] < inf # Neighborhood membership
True
>>> W[c][e] < inf # Neighborhood membership
False
>>> sum(1 for w in W[a] if w < inf) - 1 # Degree
5

Note that 1 is subtracted from the degree sum, because we don’t want to count the diagonal.
Thedegree calculation here is Θ(n) , whereas both membership and degree could easily be found in constant time with the proper structure.
Again, you should always keep in mind how you are going to use your graph and represent it accordingly


2.Trees

Trees are just a special kind of graphs, so most algorithms and
representations for graphs will work for them as well.
However, because of their special properties (they are connected and have no cycles),
some specialized (and quite simple) versions of both the representations and algorithms are possible.

2.1 Implementing trees

treesample

Method1.represent that tree with lists of lists

>>> T = [[“a”, “b”], [“c”], [“d”, [“e”, “f”]]]
>>> T[0][1]
‘b’
>>> T[2][1][0]
‘e’

Each list is, in a way, a neighbor (or child) list of the (anonymous) internal nodes.
In the second example, we access the third child of the root, the second child of that child, and finally the first child of that (path highlighted in the figure).

Method2.A Binary Tree Class
A binary tree is one where each internal node has a maximum of two children.

class Tree:
    def __init__(self, left, right):
        self.left = left
        self.right = right

You can use the Tree class like this:

>>> t = Tree(Tree(“a”, “b”), Tree(“c”, “d”))
>>> t.right.left
‘c

Method3.A Multiway Tree Class
A common way of implementing trees, especially in languages that don’t have built-in lists, is the first child, next sibling representation.

Each tree node refers to a linked list of siblings (its children), and each of these siblings refers to a linked list of its own.

class Tree:
    def __init__(self, kids, next=None):
        self.kids = self.val = kids
        self.next = next

Here’s an example of how you can access this structure:

>>> t = Tree(Tree(“a”, Tree(“b”, Tree(“c”, Tree(“d”)))))
>>> t.kids.next.next.val
‘c’

And here’s what that tree looks like:
tree2


Postscript
I copied a lot directly from the book. And I feel it’s really not easy to summarize my own unique conclusion with this kind of mature topic.
I guess I should concentrate more on the specific implemention methods , and ignore those theoretical details .I don’t really have that much time.
Fighting~!

Reference
《Python Algorithms》by Magnus Lie Hetland.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章