Vegyünk egy különböző valós számokból álló $n$-elemű listát. Keressünk meg benne egy adott számot. Végigmenni az elemeken amíg meg nem találjuk, átlagosan $n/2$ összehasonlítást, legrosszabb esetben $n$ összehasonlítást igényel. Ez az algoritmus tehát a lista hosszának konstansszorosányi műveletettel megoldható, amit úgy fejezünk ki, hogy az algoritmus a műveletigénye a lista hosszában lineáris, azaz $O(n)$ (estsd: ordó $n$).
Ennél lehet gyorsabban is, a bináris kereséssel, ha a lista már rendezve van. Ez $O(\log(n))$ idő alatt megtalálja a listában a keresett elemet. Akkor érdemes használni, ha több elemet is meg kell keresni a listában: ekkor egyszer kell rendezni, utána már gyorsan lehet benne keresni elemeket.
Bináris keresés
import sys
def binary_search(l, x):
a = 0
b = len(l)-1
while a <= b:
i = (a + b)//2
print(i, end=' ', file=sys.stderr)
if l[i] < x:
a = i+1
elif l[i] > x:
b = i-1
else:
print(file=sys.stderr)
return i
print(file=sys.stderr)
return None
stderr
(standard error
) a Linux redszereken az a file ahová egy processz írni tudja a hibaüzeneteket. A terminálról futó processzek esetén ez a terminálon jelenik meg a stdout
(standard output
) kimenetekkel együtt. A jupyter halvány színnel különbözteti meg az outputtól. A fenti kódban nyomkövetésre használjuk, elkülönítendő a standard kimenettől.
l = [3, 10, -1, 3.14, 20, 2, 11, 12, 1]
l.sort()
print(l)
b = binary_search(l, 10)
print(b, l[b])
b = binary_search(l, 13)
print(b)
Ez a keresés legrosszabb esetben $\log_2(n)$ felső egészrésze lépést vesz igénybe, mert minden lépésben feleződik a lehetséges helyek száma.
for i in range(-1, 11):
print(binary_search(list(range(10)), i), end=" ")
Ebben az algoritmusban egy fontos kritérium, hogy előre rendezve legyen a lista. Kérdés, mit tegyünk, ha egy új elemet akarunk a listába tenni. Ezt könnyítendő megtehetjük, hogy úgy szúrunk be elemet, hogy az a megfelelő helyre kerüljön és akkor mindvégig rendezve is marad a lista (törlésnél nem romlik el a rendezés).
Írjunk osztályt rendezett lista kezelésére.
class OrderedList:
def __init__(self, elements=[]):
self.l = sorted(elements)
def search(self, x):
a = 0
b = len(self.l)-1
while a <= b:
i = (a + b)//2
if self.l[i] < x:
a = i+1
elif self.l[i] > x:
b = i-1
else:
return i
return None
def insert(self, x):
a = 0
b = len(self.l)-1
while a <= b:
i = (a + b)//2
if self.l[i] < x:
a = i+1
elif self.l[i] > x:
b = i-1
else:
return
self.l.insert(a, x)
def erase(self, index):
if index >= 0 and index < len(self.l):
del self.l[index]
def __iter__(self):
return iter(self.l)
def __repr__(self):
return str(self.l)
l = OrderedList([1, -1, 3])
print(l)
l.insert(5)
print(l)
l.erase(2)
print(l)
l.insert(3.14)
l.insert(0)
print(l)
print(l.search(1))
A bináris fa egy olyan irányított gráf, amelynek vagy egy gyökere (egy kitüntetett csúcs, melynek 0 a befoka, a többi csúcsnak 1 a befoka), továbbá minden csúcsnak legfeljebb két „gyereke” van (azaz minden csúcs kifoka legföljebb 2). Az elemek sorrendben tartását egy bináris fával fogjuk megvalósítani elemek hozzáadása és törlése közben is.
class Tree:
def __init__(self, root):
self.root = root
self.left = None
self.right = None
def insert(self, data):
if self.root > data:
if self.left is None:
self.left = Tree(data)
else:
self.left.insert(data)
elif self.root < data:
if self.right is None:
self.right = Tree(data)
else:
self.right.insert(data)
A bináris fa jelen implementációja eleve rendezve tárol elemeket a fa csúcsaiban. A módszer az, hogy a csúcs üres (None
értékű) bal- és jobb gyerekkel jön létre. Ha be akarunk szúrni egy elemet, akkor a következőképp járunk el:
Most bővítsük ki az osztályunkat egy to_list()
metódussal. Ez a fa elemeit rendezett listaként adja vissza. Először létrehozunk egy üres listát. Ha a bal ág nem üres, meghívjuk rekurzívan a metódust, és hozzáadjuk a listához a visszaadott listát, majd hozzáadjuk a listához az aktuális Tree
adatát, majd a jobb ágra is meghívjuk a to_list
metódust és a visszaadott listát hozzáadjuk a eddigiekhez.
class Tree:
def __init__(self, root):
self.root = root
self.left = None
self.right = None
def insert(self, data):
if self.root > data:
if self.left is None:
self.left = Tree(data)
else:
self.left.insert(data)
elif self.root < data:
if self.right is None:
self.right = Tree(data)
else:
self.right.insert(data)
def to_list(self):
s = []
if self.left is not None:
s += self.left.to_list()
s.append(self.root)
if self.right is not None:
s += self.right.to_list()
return s
tree = Tree(5)
tree.insert(4)
tree.insert(1)
tree.insert(7)
print(tree.to_list())
5
/ \
4 7
/
1
Mit nyertünk ezzel? Először is, az adatainkat rendezve tároljuk, beszúrni pedig átlagosan $O(\log(n))$ lépésben tudunk, hiszen a fa mélysége nagyjából ennyi. Persze ez nem mindig igaz, hiszen ha az adataink eleve rendezve érkeznek, a fa nem lesz kiegyensúlyozott. Nézzük meg a pythontutor-on!
tree = Tree(5)
tree.insert(4)
tree.insert(3)
tree.insert(2)
tree.insert(1)
5
/
4
/
3
/
2
/
1
Önkiegyensúlyozó, AVL-fákról az Algoritmuselmélet tárgy keretein belül lesz szó. Most implementáljuk a find(data)
metódust, ami visszaadja azt a részfát, melynek a data
a gyökere.
class Tree:
def __init__(self, root):
self.root = root
self.left = None
self.right = None
def insert(self, data):
if self.root > data:
if self.left is None:
self.left = Tree(data)
else:
self.left.insert(data)
elif self.root < data:
if self.right is None:
self.right = Tree(data)
else:
self.right.insert(data)
def to_list(self):
s = []
if self.left is not None:
s += self.left.to_list()
s.append(self.root)
if self.right is not None:
s += self.right.to_list()
return s
def find(self, data):
if data == self.root:
return self
elif data < self.root and self.left is not None:
return self.left.find(data)
elif data > self.root and self.right is not None:
return self.right.find(data)
else:
return None
tree = Tree(5)
tree.insert(3)
tree.insert(6)
tree.insert(4)
tree.insert(1)
tree.insert(2)
print(tree.find(3).root)
print(tree.find(3).to_list())
5
/ \
3 6
/ \
1 4
\
2
Hogyan törölhetünk egy csúcsot a fából? Ha a törlendő adat egy levélben van (nincs gyereke, azaz a kifoka 0), vagy csak egy gyereke van, a helyzet egyszerű: kitöröljük és az egyetlen gyerekével (ha egyáltalán létezik) helyettesítjük. Ha két gyereke van, megkeressük a legkisebb elemet, ami még nagyobb nála (ez a leg-baloldalibb levél a jobb oldali részfában) és azzal helyettesítjük, a levelet pedig töröljük.
class Tree:
def __init__(self, root):
self.root = root
self.left = None
self.right = None
def insert(self, data):
if self.root > data:
if self.left is None:
self.left = Tree(data)
else:
self.left.insert(data)
elif self.root < data:
if self.right is None:
self.right = Tree(data)
else:
self.right.insert(data)
def to_list(self):
s = []
if self.left is not None:
s += self.left.to_list()
s.append(self.root)
if self.right is not None:
s += self.right.to_list()
return s
def minValue(self):
current = self
while(current.left is not None):
current = current.left
return current
def deleteNode(tree, node):
if node < tree.root:
tree.left = deleteNode(tree.left, node)
elif node > tree.root:
tree.right = deleteNode(tree.right, node)
else:
if tree.left is None:
temp = tree.right
tree = None
return temp
elif tree.right is None:
temp = tree.left
tree = None
return temp
temp = tree.right.minValue()
tree.root = temp.root
tree.right = deleteNode(tree.right, temp.root)
return tree
tree = Tree(5)
tree.insert(3)
tree.insert(1)
tree.insert(4)
tree = deleteNode(tree, 3)
print(tree.to_list())
5 5
/ /
3 => 4
/ \ /
1 4 1
tree = Tree(5)
tree.insert(4); tree.insert(1); tree.insert(8)
tree.insert(6); tree.insert(9)
print(tree.root, tree.to_list())
tree = deleteNode(tree, 5); print(tree.root, tree.to_list())
tree = deleteNode(tree, 6)
tree = deleteNode(tree, 9); print(tree.root, tree.to_list())
tree = deleteNode(tree, 1)
tree = deleteNode(tree, 8); print(tree.root, tree.to_list())
5 6 8 8 8 4
/ \ / \ / \ / /
4 8 => 4 8 => 4 9 => 4 => 4 =>
/ / \ / \ / /
1 6 9 1 9 1 1
Például formulák elemzésére, elvégzésére. Az alábbi fa olyan, hogy vagy két gyereke van egy csúcsnak, vagy egy sem. Ha két gyereke van, akkor a csúcs egy műveletet reprezentál, ha nincs gyereke, akkor egy számot. Az alábbi kód nagyon le van egyszerűsítve, de talán világos belőle, hogy pl. matematikai formulát hogy lehet feldolgozni.
class Node:
def __init__(self, data):
self.data = data
def calculate(self):
if self.data == "+":
return self.left.calculate() + self.right.calculate()
elif self.data == "*":
return self.left.calculate() * self.right.calculate()
else:
return self.data
def __str__(self):
if self.data == "+" or self.data == "*":
return f"({self.left.__str__()}){self.data}({self.right.__str__()})"
else:
return str(self.data)
def __repr__(self):
return self.__str__()
(7*(2+3))+(-3)
+ x0
/ \ / \
* -3 x1 x4
/ \ / \
7 + x2 x3
/ \ / \
2 3 x5 x6
x0 = Node("+"); x1 = Node("*"); x3 = Node("+")
x2 = Node(7); x4 = Node(-3); x5 = Node(2); x6 = Node(3)
x0.left = x1; x0.right = x4
x1.left = x2; x1.right = x3
x3.left = x5; x3.right = x6
print(x0.calculate())
x0
x1
x5