grscheller.datastructures.nodes

Nodes for Graphs

Node classes used with graph-like data structures. API designed to be used by other data structures which contain these data structures.

Node types

  • class SL_Node:
  • class DL_Node:
  • class Tree_Node:
  1# Copyright 2023-2024 Geoffrey R. Scheller
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#     http://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""
 16### Nodes for Graphs
 17
 18Node classes used with graph-like data structures. API designed to be used by
 19other data structures which contain these data structures.
 20
 21#### Node types
 22
 23* class **SL_Node:**
 24* class **DL_Node:**
 25* class **Tree_Node:**
 26
 27"""
 28from __future__ import annotations
 29from collections.abc import Callable, Iterator
 30from typing import Callable, cast, Iterator
 31from grscheller.fp.err_handling import MB
 32
 33__all__ = ['SL_Node', 'DL_Node', 'Tree_Node']
 34
 35class SL_Node[D]():
 36    """Data node for rearward Pointing (tip-to-root) singularly linked graphs.
 37
 38    * for mutable and immutable linear data structures
 39    * designed so multiple instances can safely share the same data
 40    * this type of node always contain data and optionally a previous Node
 41    * nodes point towards a unique "root node" with no predecessor
 42      * in a Boolean context return false only if only at a root
 43      * multiple root nodes can exist
 44      * empty data structures can be "re-rooted"
 45    * two nodes compare as equal if
 46      * both their previous Nodes are the same
 47      * their data compares as equal
 48    * more than one node can point to the same node forming bush like graphs
 49
 50    """
 51    __slots__ = '_data', '_prev'
 52
 53    def __init__(self, data: D, prev: MB[SL_Node[D]]) -> None:
 54        self._data = data
 55        self._prev = prev
 56
 57    def __iter__(self) -> Iterator[D]:
 58        node = self
 59        while node:
 60            yield node._data
 61            node = node._prev.get()
 62        yield node._data
 63
 64    def __bool__(self) -> bool:
 65        return self._prev != MB()
 66
 67    def data_eq(self, other: SL_Node[D]) -> bool:
 68        if self._data is other._data:
 69            return True
 70        elif self._data == other._data:
 71            return True
 72        else:
 73            return False
 74
 75    def __eq__(self, other: object) -> bool:
 76        if not isinstance(other, type(self)):
 77            return False
 78
 79        if self._prev is not other._prev:
 80            return False
 81        else:
 82            return self.data_eq(other)
 83
 84    def get_data(self) -> D:
 85        return self._data
 86
 87    def fold[T](self,  f: Callable[[T, D], T], init: T|None=None) -> T:
 88        """Reduce data across linked nodes.
 89
 90        * with a function and an optional starting value
 91        * reduces in natural LIFO order
 92          * from self to the root
 93
 94        """
 95        if init is None:
 96            acc: T = cast(T, self._data)
 97            node = self._prev.get()
 98        else:
 99            acc = init
100            node = self
101
102        while node:
103            acc = f(acc, node._data)
104            node = node._prev.get()
105        acc = f(acc, node._data)
106        return acc
107
108    def pop2(self) -> tuple[D, MB[SL_Node[D]]]:
109        """Return the *head* and, if it exists, the top node of the *tail*."""
110        return self._data, self._prev
111
112    def push_data(self, data: D) -> SL_Node[D]:
113        """Push data onto the stack and return a new node containing the data."""
114        return SL_Node(data, MB(self))
115
116class DL_Node[D]():
117    """Doubly Linked Node.
118
119    Doubly linked nodes for graph-like data structures.
120
121    * this type of node always contain data, even if that data is None
122      * in a Boolean context return true if both left and right nodes exist
123    * doubly link lists possible
124    * circular graphs are possible
125    * simple recursive binary trees possible
126
127    """
128    __slots__ = '_left', '_data', '_right'
129
130    def __init__(self, left: MB[DL_Node[D]], data: D, right: MB[DL_Node[D]]):
131        self._left = left
132        self._data = data
133        self._right = right
134
135    def __bool__(self) -> bool:
136        if self._left == MB() or self._right == MB():
137            return False
138        return True
139
140    def __eq__(self, other: object) -> bool:
141        if not isinstance(other, type(self)):
142            return False
143
144        if self._left is not other._left:
145            return False
146        if self._right is not other._right:
147            return False
148        if self._data is other._data:
149            return True
150        elif self._data == other._data:
151            return True
152
153        return False
154
155    def has_left(self) -> bool:
156        return self._left != MB()
157
158    def has_right(self) -> bool:
159        return self._right != MB()
160
161class Tree_Node[D, M]():
162    """Binary Tree Node with metadata.
163
164    Nodes useful for binary trees.
165
166    * this type of node always contain data, even if that data is None
167    * in a Boolean context return true if not at the top of the tree
168    * potential uses of metadata can be for re-balancing or repeat counts
169    """
170    __slots__ = '_data', '_left', '_right', '_up'
171
172    def __init__(self, data: D,
173                 up: MB[Tree_Node[D,M]],
174                 left: MB[Tree_Node[D,M]],
175                 right: MB[Tree_Node[D,M]],
176                 meta: tuple[M, ...] = ()):
177        self._data = data
178        self._up = up
179        self._left = left
180        self._right = right
181
182    def __bool__(self) -> bool:
183        if self._up == MB():
184            return False
185        else:
186            return True
187
188    def is_top(self) -> bool:
189        return self._up == MB()
class SL_Node(typing.Generic[D]):
 36class SL_Node[D]():
 37    """Data node for rearward Pointing (tip-to-root) singularly linked graphs.
 38
 39    * for mutable and immutable linear data structures
 40    * designed so multiple instances can safely share the same data
 41    * this type of node always contain data and optionally a previous Node
 42    * nodes point towards a unique "root node" with no predecessor
 43      * in a Boolean context return false only if only at a root
 44      * multiple root nodes can exist
 45      * empty data structures can be "re-rooted"
 46    * two nodes compare as equal if
 47      * both their previous Nodes are the same
 48      * their data compares as equal
 49    * more than one node can point to the same node forming bush like graphs
 50
 51    """
 52    __slots__ = '_data', '_prev'
 53
 54    def __init__(self, data: D, prev: MB[SL_Node[D]]) -> None:
 55        self._data = data
 56        self._prev = prev
 57
 58    def __iter__(self) -> Iterator[D]:
 59        node = self
 60        while node:
 61            yield node._data
 62            node = node._prev.get()
 63        yield node._data
 64
 65    def __bool__(self) -> bool:
 66        return self._prev != MB()
 67
 68    def data_eq(self, other: SL_Node[D]) -> bool:
 69        if self._data is other._data:
 70            return True
 71        elif self._data == other._data:
 72            return True
 73        else:
 74            return False
 75
 76    def __eq__(self, other: object) -> bool:
 77        if not isinstance(other, type(self)):
 78            return False
 79
 80        if self._prev is not other._prev:
 81            return False
 82        else:
 83            return self.data_eq(other)
 84
 85    def get_data(self) -> D:
 86        return self._data
 87
 88    def fold[T](self,  f: Callable[[T, D], T], init: T|None=None) -> T:
 89        """Reduce data across linked nodes.
 90
 91        * with a function and an optional starting value
 92        * reduces in natural LIFO order
 93          * from self to the root
 94
 95        """
 96        if init is None:
 97            acc: T = cast(T, self._data)
 98            node = self._prev.get()
 99        else:
100            acc = init
101            node = self
102
103        while node:
104            acc = f(acc, node._data)
105            node = node._prev.get()
106        acc = f(acc, node._data)
107        return acc
108
109    def pop2(self) -> tuple[D, MB[SL_Node[D]]]:
110        """Return the *head* and, if it exists, the top node of the *tail*."""
111        return self._data, self._prev
112
113    def push_data(self, data: D) -> SL_Node[D]:
114        """Push data onto the stack and return a new node containing the data."""
115        return SL_Node(data, MB(self))

Data node for rearward Pointing (tip-to-root) singularly linked graphs.

  • for mutable and immutable linear data structures
  • designed so multiple instances can safely share the same data
  • this type of node always contain data and optionally a previous Node
  • nodes point towards a unique "root node" with no predecessor
    • in a Boolean context return false only if only at a root
    • multiple root nodes can exist
    • empty data structures can be "re-rooted"
  • two nodes compare as equal if
    • both their previous Nodes are the same
    • their data compares as equal
  • more than one node can point to the same node forming bush like graphs
SL_Node(data: 'D', prev: 'MB[SL_Node[D]]')
54    def __init__(self, data: D, prev: MB[SL_Node[D]]) -> None:
55        self._data = data
56        self._prev = prev
def data_eq(self, other: 'SL_Node[D]') -> bool:
68    def data_eq(self, other: SL_Node[D]) -> bool:
69        if self._data is other._data:
70            return True
71        elif self._data == other._data:
72            return True
73        else:
74            return False
def get_data(self) -> 'D':
85    def get_data(self) -> D:
86        return self._data
def fold(self, f: 'Callable[[T, D], T]', init: 'T | None' = None) -> 'T':
 88    def fold[T](self,  f: Callable[[T, D], T], init: T|None=None) -> T:
 89        """Reduce data across linked nodes.
 90
 91        * with a function and an optional starting value
 92        * reduces in natural LIFO order
 93          * from self to the root
 94
 95        """
 96        if init is None:
 97            acc: T = cast(T, self._data)
 98            node = self._prev.get()
 99        else:
100            acc = init
101            node = self
102
103        while node:
104            acc = f(acc, node._data)
105            node = node._prev.get()
106        acc = f(acc, node._data)
107        return acc

Reduce data across linked nodes.

  • with a function and an optional starting value
  • reduces in natural LIFO order
    • from self to the root
def pop2(self) -> 'tuple[D, MB[SL_Node[D]]]':
109    def pop2(self) -> tuple[D, MB[SL_Node[D]]]:
110        """Return the *head* and, if it exists, the top node of the *tail*."""
111        return self._data, self._prev

Return the head and, if it exists, the top node of the tail.

def push_data(self, data: 'D') -> 'SL_Node[D]':
113    def push_data(self, data: D) -> SL_Node[D]:
114        """Push data onto the stack and return a new node containing the data."""
115        return SL_Node(data, MB(self))

Push data onto the stack and return a new node containing the data.

class DL_Node(typing.Generic[D]):
117class DL_Node[D]():
118    """Doubly Linked Node.
119
120    Doubly linked nodes for graph-like data structures.
121
122    * this type of node always contain data, even if that data is None
123      * in a Boolean context return true if both left and right nodes exist
124    * doubly link lists possible
125    * circular graphs are possible
126    * simple recursive binary trees possible
127
128    """
129    __slots__ = '_left', '_data', '_right'
130
131    def __init__(self, left: MB[DL_Node[D]], data: D, right: MB[DL_Node[D]]):
132        self._left = left
133        self._data = data
134        self._right = right
135
136    def __bool__(self) -> bool:
137        if self._left == MB() or self._right == MB():
138            return False
139        return True
140
141    def __eq__(self, other: object) -> bool:
142        if not isinstance(other, type(self)):
143            return False
144
145        if self._left is not other._left:
146            return False
147        if self._right is not other._right:
148            return False
149        if self._data is other._data:
150            return True
151        elif self._data == other._data:
152            return True
153
154        return False
155
156    def has_left(self) -> bool:
157        return self._left != MB()
158
159    def has_right(self) -> bool:
160        return self._right != MB()

Doubly Linked Node.

Doubly linked nodes for graph-like data structures.

  • this type of node always contain data, even if that data is None
    • in a Boolean context return true if both left and right nodes exist
  • doubly link lists possible
  • circular graphs are possible
  • simple recursive binary trees possible
DL_Node(left: 'MB[DL_Node[D]]', data: 'D', right: 'MB[DL_Node[D]]')
131    def __init__(self, left: MB[DL_Node[D]], data: D, right: MB[DL_Node[D]]):
132        self._left = left
133        self._data = data
134        self._right = right
def has_left(self) -> bool:
156    def has_left(self) -> bool:
157        return self._left != MB()
def has_right(self) -> bool:
159    def has_right(self) -> bool:
160        return self._right != MB()
class Tree_Node(typing.Generic[D, M]):
162class Tree_Node[D, M]():
163    """Binary Tree Node with metadata.
164
165    Nodes useful for binary trees.
166
167    * this type of node always contain data, even if that data is None
168    * in a Boolean context return true if not at the top of the tree
169    * potential uses of metadata can be for re-balancing or repeat counts
170    """
171    __slots__ = '_data', '_left', '_right', '_up'
172
173    def __init__(self, data: D,
174                 up: MB[Tree_Node[D,M]],
175                 left: MB[Tree_Node[D,M]],
176                 right: MB[Tree_Node[D,M]],
177                 meta: tuple[M, ...] = ()):
178        self._data = data
179        self._up = up
180        self._left = left
181        self._right = right
182
183    def __bool__(self) -> bool:
184        if self._up == MB():
185            return False
186        else:
187            return True
188
189    def is_top(self) -> bool:
190        return self._up == MB()

Binary Tree Node with metadata.

Nodes useful for binary trees.

  • this type of node always contain data, even if that data is None
  • in a Boolean context return true if not at the top of the tree
  • potential uses of metadata can be for re-balancing or repeat counts
Tree_Node( data: 'D', up: 'MB[Tree_Node[D, M]]', left: 'MB[Tree_Node[D, M]]', right: 'MB[Tree_Node[D, M]]', meta: 'tuple[M, ...]' = ())
173    def __init__(self, data: D,
174                 up: MB[Tree_Node[D,M]],
175                 left: MB[Tree_Node[D,M]],
176                 right: MB[Tree_Node[D,M]],
177                 meta: tuple[M, ...] = ()):
178        self._data = data
179        self._up = up
180        self._left = left
181        self._right = right
def is_top(self) -> bool:
189    def is_top(self) -> bool:
190        return self._up == MB()