~tieong/runestone-python

217b36749eeaf70e68a86e8b78505ff0b089dc81 — Thomas Ieong 1 year, 7 months ago master
Init
A  => README.org +7 -0
@@ 1,7 @@
* Runestone

See https://runestone.academy/ns/books/published//pythonds/index.html

These repos are my answers to the exercises in the book.

Still need to look at the 8th and ninth chapter.

A  => chapter1/README.org +6 -0
@@ 1,6 @@
* Chapter 1

Not much to do here, since I'm already confortable with python I chose
to skip some of the exercises.

And since I'm already doing the NAND book with the logic gates I got a bit lazy.

A  => chapter1/fraction.py +46 -0
@@ 1,46 @@
#!/usr/bin/python3


"""
The goal here is to override magic method for our Fraction object
like gt,lt,sub,multiply,divide...
"""

def gcd(m,n):
    while m%n != 0:
        oldm = m
        oldn = n

        m = oldn
        n = oldm%oldn
    return n

class Fraction:
     def __init__(self,top,bottom):
         self.num = top
         self.den = bottom

     def __str__(self):
         return str(self.num)+"/"+str(self.den)

     def show(self):
         print(self.num,"/",self.den)

     def __add__(self,otherfraction):
         newnum = self.num*otherfraction.den + \
                      self.den*otherfraction.num
         newden = self.den * otherfraction.den
         common = gcd(newnum,newden)
         return Fraction(newnum//common,newden//common)

     def __eq__(self, other):
         firstnum = self.num * other.den
         secondnum = other.num * self.den

         return firstnum == secondnum

x = Fraction(1,2)
y = Fraction(2,3)
print(x+y)
print(x == y)


A  => chapter1/logic_gates.py +143 -0
@@ 1,143 @@
#!/usr/bin/python3

"""
Create a two new gate classes, one called NorGate the other called
NandGate. NandGates work like AndGates that have a Not attached to the
output. NorGates work lake OrGates that have a Not attached to the
output.

Create a series of gates that prove the following equality NOT (( A
and B) or (C and D)) is that same as NOT( A and B ) and NOT (C and
D). Make sure to use some of your new gates in the simulation.
"""

class LogicGate:

    def __init__(self,n):
        self.name = n
        self.output = None

    def getLabel(self):
        return self.name

    def getOutput(self):
        self.output = self.performGateLogic()
        return self.output


class BinaryGate(LogicGate):

    def __init__(self,n):
        super(BinaryGate, self).__init__(n)

        self.pinA = None
        self.pinB = None

    def getPinA(self):
        if self.pinA == None:
            return int(input("Enter Pin A input for gate "+self.getLabel()+"-->"))
        else:
            return self.pinA.getFrom().getOutput()

    def getPinB(self):
        if self.pinB == None:
            return int(input("Enter Pin B input for gate "+self.getLabel()+"-->"))
        else:
            return self.pinB.getFrom().getOutput()

    def setNextPin(self,source):
        if self.pinA == None:
            self.pinA = source
        else:
            if self.pinB == None:
                self.pinB = source
            else:
                print("Cannot Connect: NO EMPTY PINS on this gate")


class AndGate(BinaryGate):

    def __init__(self,n):
        BinaryGate.__init__(self,n)

    def performGateLogic(self):

        a = self.getPinA()
        b = self.getPinB()
        if a==1 and b==1:
            return 1
        else:
            return 0

class OrGate(BinaryGate):

    def __init__(self,n):
        BinaryGate.__init__(self,n)

    def performGateLogic(self):

        a = self.getPinA()
        b = self.getPinB()
        if a ==1 or b==1:
            return 1
        else:
            return 0

class UnaryGate(LogicGate):

    def __init__(self,n):
        LogicGate.__init__(self,n)

        self.pin = None

    def getPin(self):
        if self.pin == None:
            return int(input("Enter Pin input for gate "+self.getLabel()+"-->"))
        else:
            return self.pin.getFrom().getOutput()

    def setNextPin(self,source):
        if self.pin == None:
            self.pin = source
        else:
            print("Cannot Connect: NO EMPTY PINS on this gate")


class NotGate(UnaryGate):

    def __init__(self,n):
        UnaryGate.__init__(self,n)

    def performGateLogic(self):
        if self.getPin():
            return 0
        else:
            return 1


class Connector:

    def __init__(self, fgate, tgate):
        self.fromgate = fgate
        self.togate = tgate

        tgate.setNextPin(self)

    def getFrom(self):
        return self.fromgate

    def getTo(self):
        return self.togate


def main():
   g1 = AndGate("G1")
   g2 = AndGate("G2")
   g3 = OrGate("G3")
   g4 = NotGate("G4")
   c1 = Connector(g1,g3)
   c2 = Connector(g2,g3)
   c3 = Connector(g3,g4)
   print(g4.getOutput())

main()

A  => chapter1/monkey_type.py +83 -0
@@ 1,83 @@
#!/usr/bin/python3

"""The monkey that types a text if given an infinite amount of time."""

import random
import string


def generate_phrase(best_phrase: str = None) -> str:
    """Generate a random 28 string phrase."""
    string_space = string.ascii_lowercase + " "
    phrase = ""
    if best_phrase:
        phrase = best_phrase
    while len(phrase) != 28:
        letter = random.choice(string_space)
        phrase = phrase + letter
    return phrase


def generate_from_best_phrase(best_phrase: str, score: float) -> str:
    """Generate a phrase from the correct letters gathered so far."""
    if score == 0:
        generated_best_phrase = generate_phrase()
    else:
        generated_best_phrase = generate_phrase(best_phrase)
    return generated_best_phrase


def generate_score(
        generated_string: str,
        target: str
) -> tuple[float, str]:
    """Score the generated_phrase compared to the target"""
    letters_matched = 0
    letters_correct = ""
    for letter_generated, letter_target in zip(generated_string, target):
        if letter_generated == letter_target:
            letters_matched += 1
            letters_correct = letters_correct + letter_generated
        else:
            break
    score = (letters_matched / len(generated_string)) * 100
    return score, letters_correct


def infinite_monkey():
    """This should produce a phrase from Shakespeare given time."""
    phrases_generated = {}
    best_phrase = ""
    best_score = 0.0
    phrase = generate_phrase()
    score, letters_correct = generate_score(
        phrase,
        "methinks it is like a weasel"
    )
    phrases_generated[phrase] = score
    count = 0
    while score != 100:
        best_phrase = max(phrases_generated, key=phrases_generated.get)
        best_score = phrases_generated[best_phrase]
        phrase = generate_from_best_phrase(
            letters_correct, score
        )
        score, letters_correct = generate_score(
            phrase,
            "methinks it is like a weasel"
        )
        if phrase not in phrases_generated:
            phrases_generated[phrase] = score
        if count % 1000 == 0:
            print(f"{count} : {best_phrase} {best_score:.2f} %")
        count += 1
    print(f"{count} : {phrase} {score:.2f} %")


def main():
    """Entry point."""
    infinite_monkey()


if __name__ == '__main__':
    main()

A  => chapter2/README.org +4 -0
@@ 1,4 @@
* Chapter 2

This one mostly talks about override of standard methods, pitfalls
and exercice about it

A  => chapter2/dice.py +55 -0
@@ 1,55 @@
#!/usr/bin/python3


"""PYthon implements the negation of __eq__ if ne not implemented
same with ohter operators"""

import random

class MSDie:
    """
    Multi-sided die

    Instance Variables:
        current_value
        num_sides

    """

    def __init__(self, num_sides):
        self.num_sides = num_sides
        self.current_value = self.roll()

    def roll(self):
        self.current_value = random.randrange(1,self.num_sides+1)
        return self.current_value

    def __str__(self):
        return str(self.current_value)

    def __repr__(self):
        return "MSDie({}) : {}".format(self.num_sides, self.current_value)

    def __eq__(self,other):
        return self.current_value == other.current_value

    def __lt__(self,other):
        return self.current_value < other.current_value

    def __le__(self, other):
        return self.current_value <= other.current_value


x = MSDie(6)
y = MSDie(7)

x.current_value = 6
y.current_value = 5

print(x == y)
print(x < y)
print(x > y)
print(x != y)
print(x<=y)
print(x>=y)
print(x is y)

A  => chapter3/README.org +59 -0
@@ 1,59 @@
* Chapter 3

This one 's about algorithm analysis

The big O to compare algorith mregardless of their implementation in language,compiler,time...

Although we do not see this in the summation example, sometimes the performance of an algorithm depends on the exact values of the data rather than simply the size of the problem. For these kinds of algorithms we need to characterize their performance in terms of best case, worst case, or average case performance. The worst case performance refers to a particular data set where the algorithm performs especially poorly. Whereas a different data set for the exact same algorithm might have extraordinarily good performance. However, in most cases the algorithm performs somewhere in between these two extremes (average case). It is important for a computer scientist to understand these distinctions so they are not misled by one particular case.

a=5
b=6
c=10
for i in range(n):
   for j in range(n):
      x = i * i
      y = j * j
      z = i * j
for k in range(n):
   w = a*k + 45
   v = b*b
d = 33

The number of assignment operations is the sum of four terms. The first term is the constant 3, representing the three assignment statements at the start of the fragment. The second term is 3n2
, since there are three statements that are performed n2 times due to the nested iteration. The third term is 2n, two statements iterated n times. Finally, the fourth term is the constant 1, representing the final assignment statement. This gives us T(n)=3+3n2+2n+1=3n2+2n+4. By looking at the exponents, we can easily see that the n2 term will be dominant and therefore this fragment of code is O(n2). Note that all of the other terms as well as the coefficient on the dominant term can be ignored as n grows larger.

if nested loop
o(n) to the power n

if not nested
o(n)

see iflog(n) number of steps get reduced as algo goes on




    Algorithm analysis is an implementation-independent way of measuring an algorithm.

    Big-O notation allows algorithms to be classified by their dominant process with respect to the size of the problem.


    how to use the timeit module, test function over big range, plot graphically to check time complexity

    also depends on implementation for big oh


import timeit
import random

for i in range(10000,1000001,20000):
    t = timeit.Timer("random.randrange(%d) in x"%i,
                     "from __main__ import random,x")
    x = list(range(i))
    lst_time = t.timeit(number=1000)
    x = {j:None for j in range(i)}
    d_time = t.timeit(number=1000)
    print("%d,%10.3f,%10.3f" % (i, lst_time, d_time))


    Though this works only for small snippets, other tools for real project

A  => chapter3/anagram.py +79 -0
@@ 1,79 @@
#!/usr/bin/python3

# n^2 solution

def anagramSolution1(s1,s2):
    stillOK = True
    if len(s1) != len(s2):
        stillOK = False

    alist = list(s2)
    pos1 = 0

    while pos1 < len(s1) and stillOK:
        pos2 = 0
        found = False
        while pos2 < len(alist) and not found:
            if s1[pos1] == alist[pos2]:
                found = True
            else:
                pos2 = pos2 + 1

        if found:
            alist[pos2] = None
        else:
            stillOK = False

        pos1 = pos1 + 1

    return stillOK

print(anagramSolution1('abcd','dcba'))

# depends on the sort

def anagramSolution2(s1,s2):
    alist1 = list(s1)
    alist2 = list(s2)

    alist1.sort()
    alist2.sort()

    pos = 0
    matches = True

    while pos < len(s1) and matches:
        if alist1[pos]==alist2[pos]:
            pos = pos + 1
        else:
            matches = False

    return matches

print(anagramSolution2('abcde','edcba'))

# o(n), last loop is always 26

def anagramSolution4(s1,s2):
    c1 = [0]*26
    c2 = [0]*26

    for i in range(len(s1)):
        pos = ord(s1[i])-ord('a')
        c1[pos] = c1[pos] + 1

    for i in range(len(s2)):
        pos = ord(s2[i])-ord('a')
        c2[pos] = c2[pos] + 1

    j = 0
    stillOK = True
    while j<26 and stillOK:
        if c1[j]==c2[j]:
            j = j + 1
        else:
            stillOK = False

    return stillOK

print(anagramSolution4('apple','pleap'))

A  => chapter3/check_performance.py +56 -0
@@ 1,56 @@
#!/usr/bin/python3

import random
from timeit import Timer

# popzero = Timer("x.pop(0)",
#                 "from __main__ import x")
# popend = Timer("x.pop()",
#                "from __main__ import x")
# print("pop(0)   pop()")
# for i in range(1000000,100000001,1000000):
#     x = list(range(i))
#     pt = popend.timeit(number=1000)
#     x = list(range(i))
#     pz = popzero.timeit(number=1000)
#     print("%15.5f, %15.5f" %(pz,pt))


# """Check that the index operation of list is O(1)"""
# index_begin = Timer("x.index(0)",
#                 "from __main__ import x")
# index_last = Timer("x.index(x[-1])",
#                 "from __main__ import x")
# print("iteraton, index(0),    index(last_element)")
# for i in range(1000000,100000001,1000000):
#     x = list(range(i))
#     pt = index_begin.timeit(number=1000)
#     x = list(range(i))
#     pz = index_last.timeit(number=1000)
#     print("%d,%15.5f,%15.5f" % (i, pt, pz))


# """Check that get and set item for dict are o(1)"""
# print("Get dict time,    Set dict element time")
# for i in range(10000,1000001,20000):
#     t = Timer("x.get(%d)"%i,
#                      "from __main__ import random,x")
#     t2 = Timer("x[(%d)] = 9999"%i,
#                      "from __main__ import random,x")
#     x = {j:None for j in range(i)}
#     d_time1 = t.timeit(number=1000)
#     x = {j:None for j in range(i)}
#     d_time2 = t2.timeit(number=1000)
#     print("%d,%10.3f,%10.3f" % (i, d_time1, d_time2))

# """Compare the del operator for list and dict"""
# Should be o(1) for dict and o(n) for list
print("iteration,  del dict    del list")
for i in range(1000000,100000001,1000000):
    del_timer = Timer("del x[random.randrange(%d)]"%i,
                    "from __main__ import random, x")
    x = list(range(i))
    pt = del_timer.timeit(number=1000)
    x = {j:None for j in range(i)}
    pz = del_timer.timeit(number=1000)
    print("%d, %15.5f, %15.5f" %(i, pz,pt))

A  => chapter3/exercises.py +15 -0
@@ 1,15 @@
#!/usr/bin/python3

# Devise an experiment to verify that the list index operator is O(1)

# Devise an experiment to verify that get item and set item are O(1)

# for dictionaries.

# Devise an experiment that compares the performance of the del operator on lists and dictionaries.

# Given a list of numbers in random order, write an algorithm that works in O(nlog(n))

# to find the kth smallest number in the list.

# Can you improve the algorithm from the previous problem to be linear? Explain.

A  => chapter3/find_minimum.py +34 -0
@@ 1,34 @@
#!/usr/bin/python3

import random
from typing import List

def find_minimum(mylist: List[int]) -> int:
    """This is o(n^2)"""
    overallmin = mylist[0]
    for i in mylist:
        issmallest = True
        for j in mylist:
            if i > j:
                issmallest = False
        if issmallest:
            overallmin = i
    return overallmin


def find_minimum_optimized(mylist: List[int]) -> int:
    """This is o(n)"""
    minsofar = mylist[0]
    for i in mylist:
        if i < minsofar:
            minsofar = i
    return minsofar


def main():
    test_list = [random.randint(0,10) for x in range(10)]
    print(test_list)
    print(find_minimum(test_list))

if __name__ == '__main__':
    main()

A  => chapter4/README.org +10 -0
@@ 1,10 @@
* Linear structures

4.2. What Are Linear Structures?

We will begin our study of data structures by considering four simple but very powerful concepts. Stacks, queues, deques, and lists are examples of data collections whose items are ordered depending on how they are added or removed. Once an item is added, it stays in that position relative to the other elements that came before and came after it. Collections such as these are often referred to as linear data structures.

Linear structures can be thought of as having two ends. Sometimes these ends are referred to as the “left” and the “right” or in some cases the “front” and the “rear.” You could also call them the “top” and the “bottom.” The names given to the ends are not significant. What distinguishes one linear structure from another is the way in which items are added and removed, in particular the location where these additions and removals occur. For example, a structure might allow new items to be added at only one end. Some structures might allow items to be removed from either end.

These variations give rise to some of the most useful data structures in computer science. They appear in many algorithms and can be used to solve a variety of important problems.


A  => chapter4/dequeue.py +53 -0
@@ 1,53 @@
#!/usr/bin/python3


class Deque:
    def __init__(self):
        self.items = []

    def isEmpty(self):
        return self.items == []

    def addFront(self, item):
        self.items.append(item)

    def addRear(self, item):
        self.items.insert(0,item)

    def removeFront(self):
        return self.items.pop()

    def removeRear(self):
        return self.items.pop(0)

    def size(self):
        return len(self.items)

    def __str__(self):
        return str(self.items)


def palchecker(aString: str) -> bool:
    chardeque = Deque()

    for ch in aString:
        chardeque.addRear(ch)

    stillEqual = True

    while chardeque.size() > 1 and stillEqual:
        first = chardeque.removeFront()
        last = chardeque.removeRear()
        if first == " ":
            first = chardeque.removeFront()
        if last == " ":
            last = chardeque.removeRear()
        if first != last:
            stillEqual = False

    return stillEqual


print(palchecker("lsdkjfskf"))
print(palchecker("radar"))
print(palchecker('I PREFER PI'))

A  => chapter4/list.py +642 -0
@@ 1,642 @@
#!/usr/bin/python3

class Node:
    def __init__(self,initdata):
        self.data = initdata
        self.next = None

    def getData(self):
        return self.data

    def getNext(self):
        return self.next

    def setData(self,newdata):
        self.data = newdata

    def setNext(self,newnext):
        self.next = newnext

    def __str__(self):
        return str(self.data, str(self.next))


class UnorderedList:

    def __init__(self):
        self.head = None
        self.last = None

    def isEmpty(self):
        return self.head == None

    def add(self,item):
        temp = Node(item)
        temp.setNext(self.head)
        self.head = temp
        if not self.last:
            self.last = self.head

    def size(self):
        current = self.head
        count = 0
        while current != None:
            count = count + 1
            current = current.getNext()

        return count

    def search(self,item):
        current = self.head
        found = False
        while current != None and not found:
            if current.getData() == item:
                found = True
            else:
                current = current.getNext()

        return found

    def remove(self,item) -> None:
        current = self.head
        previous = None
        found = False
        while not found:
            if current.getData() == item:
                found = True
            else:
                previous = current
                current = current.getNext()
                if current is None:
                    break
        if previous is None:
            self.head = current.getNext()
        elif not found:
            raise ValueError("The item is not in the list")
        else:
            previous.setNext(current.getNext())

    def append(self, item) -> None:
        """
        We handle first the case where we append
        to a list without any node by setting the
        last node to the head.

        When the list is not empty we simply set the
        next pointer of the last node recorded to the given
        node and finally we update the last node to be the given
        node that is going to be appended.
        """
        temp = Node(item)
        if not self.last:
            self.head = temp
            self.last = temp
        else:
            self.last.setNext(temp)
            self.last = temp

    def pop(self, pos=None):
        """
        Remove the last element and returns the element.

        The last if else condition is to account for the case
        where the list only has one node if there is no previous node
        that means there is only one node and therefore the head should
        be set to None.

        Otherwise we change the last pointer to None
        """
        current = self.head
        previous = None
        while current:
            if not current.getNext():
                break
            previous = current
            current = current.getNext()
        if not previous:
            self.head = None
        else:
            previous.setNext(None)
        return current.getData()

    def insert(self, pos, item):
        temp = Node(item)
        current = self.head
        count = 0
        previous = None
        while current:
            if count == pos:
                break
            previous = current
            current = current.getNext()
            count += 1
        if not current and pos == 0:
            self.head = temp
        else:
            if not previous:
                temp.setNext(current)
                self.head = temp
            else:
                previous.setNext(temp)
                temp.setNext(current)

    def index(self, item) -> int:
        """-1 to have list[0] == first element"""
        index = -1
        found = False
        current = self.head
        while current and not found:
            if current.getData() == item:
                found = True
            else:
                current = current.getNext()
            index += 1
        if not found:
            index = -1
        return index

    def print_items(self) -> None:
        current = self.head
        while current:
            print(current.getData())
            current = current.getNext()

    def slice(self):
        pass

    def __str__(self):
        elements = ""
        current = self.head
        while current:
            if current == self.head:
                elements += "[" + str(current.getData())
            else:
                if current:
                    elements += ", "
                elements += str(current.getData())
            if not current.getNext():
                elements += "]"
            current = current.getNext()
        return elements

    def __getitem__(self, subscript):
        if isinstance(subscript, slice):
            # do your handling for a slice object:
            count = 0
            current = self.head
            second_list = UnorderedList()
            while count <= subscript.stop:
                if count == subscript.stop:
                    count += 1
                    break
                current = current.getNext()
                count += 1
                second_list.add(current.getData())
            return second_list
        else:
            # Do your handling for a plain index
            count = 0
            current = self.head
            second_list = UnorderedList()
            while count != subscript:
                if count == subscript:
                    count += 1
                    break
                current = current.getNext()
                count += 1
            if not current:
                raise IndexError("list index out of range")
            second_list.add(current.getData())
            return second_list

#mylist = UnorderedList()
#mylist.add(31)
#mylist.add(32)
#mylist.add(33)
#mylist.add(38)
#mylist.append(45)
#print(mylist)
#a = mylist[0:2]
#print(a)
#mylist.append(45)
#mylist.print_items()
#print('------------')
#mylist.remove(45)
#mylist.print_items()
#mylist.add(33)
#mylist.append(45)
#mylist.print_items()
##print(f"{mylist.pop()} pop result")
#print(f"{mylist.pop(2)} pop result")
#mylist.print_items()
#mylist.add(93)
#mylist.append(7524)
#mylist.append(75245)
#mylist.add(26)
#mylist.append(752455458)
#mylist.append(451)
#mylist.print_items()
#print(f"{mylist.index(752455458)} kjkljklj")
#mylist.insert(0, 654687865445754)
#mylist.insert(0, 54)
#mylist.insert(1, 20)
#mylist.print_items()
# mylist.add(26)
# mylist.add(54)
# mylist.print_items()
# 
# print(mylist.size())
# print(mylist.search(93))
# print(mylist.search(100))
# 
# mylist.add(100)
# print(mylist.search(100))
# print(mylist.size())
# 
# mylist.remove(54)
# print(mylist.size())
# mylist.remove(93)
# print(mylist.size())
# mylist.remove(31)
# print(mylist.size())
# print(mylist.search(93))


# Ordered List

class OrderedList:
    def __init__(self):
        self.head = None

    def search(self,item):
        current = self.head
        found = False
        stop = False
        while current != None and not found and not stop:
            if current.getData() == item:
                found = True
            else:
                if current.getData() > item:
                    stop = True
                else:
                    current = current.getNext()

        return found

    def add(self,item):
        current = self.head
        previous = None
        stop = False
        while current != None and not stop:
            if current.getData() > item:
                stop = True
            else:
                previous = current
                current = current.getNext()

        temp = Node(item)
        if previous == None:
            temp.setNext(self.head)
            self.head = temp
        else:
            temp.setNext(current)
            previous.setNext(temp)

    def isEmpty(self):
        return self.head == None

    def size(self):
        current = self.head
        count = 0
        while current != None:
            count = count + 1
            current = current.getNext()

        return count

    def remove(self, item) -> None:
        current = self.head
        previous = None
        found = False
        while not found:
            if current.getData() == item:
                found = True
            else:
                previous = current
                current = current.getNext()

        if previous == None:
            self.head = current.getNext()
        else:
            previous.setNext(current.getNext())

    def index(self, item) -> int:
        """-1 to have list[0] == first element"""
        index = -1
        found = False
        current = self.head
        while current and not found:
            if current.getData() == item:
                found = True
            else:
                current = current.getNext()
            index += 1
        if not found:
            index = -1
        return index

    def pop(self, pos=None):
        """
        Remove the last element and returns the element.

        The last if else condition is to account for the case
        where the list only has one node if there is no previous node
        that means there is only one node and therefore the head should
        be set to None.

        Otherwise we change the last pointer to None
        """
        current = self.head
        previous = None
        while current:
            if not current.getNext():
                break
            previous = current
            current = current.getNext()
        if not previous:
            self.head = None
        else:
            previous.setNext(None)
        return current.getData()


    def print_items(self) -> None:
        current = self.head
        while current:
            print(current.getData())
            current = current.getNext()



#mylist = OrderedList()
#mylist.add(31)
#mylist.add(77)
#mylist.add(17)
#mylist.add(93)
#mylist.add(26)
#mylist.add(54)
#mylist.print_items()
##
##print(f"{mylist.pop()} pop result")
#print(f"{mylist.index(93)} index result")
#print("-----------------------")
#mylist.print_items()

# print(mylist.size())
# print(mylist.search(93))
# print(mylist.search(100))

# Stack implemented as a linked list

class StackLinkedList():

    def __init__(self):
        self.head = None
        self.last = None

    def isEmpty(self):
        return self.head is None

    def push(self,item):
        temp = Node(item)
        temp.setNext(self.head)
        self.head = temp
        if not self.last:
            self.last = self.head

    def pop(self):
        current = self.head
        if current.getNext() is not None:
            self.head = current.getNext()
        else:
            self.head = None
        return current.getData()

    def peek(self):
        if self.size() != 0:
            return self.head.getData()

    def size(self):
        current = self.head
        count = 0
        while current is not None:
            count = count + 1
            current = current.getNext()

        return count

    def __str__(self):
        elements = ""
        current = self.head
        while current:
            if current == self.head:
                elements += "[" + str(current.getData())
            else:
                if current:
                    elements += ", "
                elements += str(current.getData())
            if not current.getNext():
                elements += "]"
            current = current.getNext()
        return elements


#stack_list = StackLinkedList()
#stack_list.push(10)
#stack_list.push(20)
#stack_list.push(30)
#print(stack_list.peek())
#print(stack_list)
#print(stack_list.size())
#print(stack_list.pop())
#print(stack_list)
#print(stack_list.peek())


# Queue linked list

class QueueLinkedList():

    def __init__(self):
        self.head = None
        self.last = None

    def isEmpty(self):
        return self.head is None

    def enqueue(self,item):
        temp = Node(item)
        temp.setNext(self.head)
        self.head = temp
        if not self.last:
            self.last = self.head

    def dequeue(self):
        current = self.head
        previous = None
        return_value = None
        if not current.getNext():
            previous = current
        while current:
            previous = current
            current = current.getNext()
        if not current:
            self.head = None
        else:
            previous.setNext(None)
            return_value = previous.getData()
        return return_value

    def size(self):
        current = self.head
        count = 0
        while current is not None:
            count = count + 1
            current = current.getNext()

        return count

    def __str__(self):
        elements = ""
        current = self.head
        while current:
            if current == self.head:
                elements += "[" + str(current.getData())
            else:
                if current:
                    elements += ", "
                elements += str(current.getData())
            if not current.getNext():
                elements += "]"
            current = current.getNext()
        return elements


#queue_list = QueueLinkedList()
#queue_list.enqueue(10)
#queue_list.enqueue(20)
#queue_list.enqueue(30)
#print(queue_list)
#print(queue_list.size())
#print(queue_list.dequeue())
#print(queue_list)

# Dequeue linked list


class DequeueLinkedList():

    def __init__(self):
        self.head = None
        self.last = None

    def isEmpty(self):
        return self.head is None

    def addFront(self,item):
        temp = Node(item)
        temp.setNext(self.head)
        self.head = temp
        if not self.last:
            self.last = self.head

    def addRear(self):
        current = self.head
        previous = None
        return_value = None
        if not current.getNext():
            previous = current
        while current:
            previous = current
            current = current.getNext()
        if not current:
            self.head = None
        else:
            previous.setNext(None)
            return_value = previous.getData()
        return return_value

    def removeFront(self):
        pass

    def removeRear(self):
        pass

    def size(self):
        current = self.head
        count = 0
        while current is not None:
            count = count + 1
            current = current.getNext()

        return count

    def __str__(self):
        elements = ""
        current = self.head
        while current:
            if current == self.head:
                elements += "[" + str(current.getData())
            else:
                if current:
                    elements += ", "
                elements += str(current.getData())
            if not current.getNext():
                elements += "]"
            current = current.getNext()
        return elements


dequeue_list = DequeueLinkedList()
dequeue_list.enqueue(10)
dequeue_list.enqueue(20)
dequeue_list.enqueue(30)

# Doubly linked list
# Head reference contains one to first node and one to last node

class DoubleNode:
    def __init__(self,initdata):
        self.data = initdata
        self.next = None
        self.back = None

    def getData(self):
        return self.data

    def getBack(self):
        return self.back

    def getNext(self):
        return self.next

    def setData(self,newdata):
        self.data = newdata

    def setBack(self, newback):
        self.back = newback

    def setNext(self,newnext):
        self.next = newnext

    def __str__(self):
        return str(self.data, str(self.back), str(self.next))


class DoublyLinkedList():
    def __init__(self, args):
        "docstring"



# Implementation of queue that has o(1) in enqueue and o(1) in dequeue

A  => chapter4/queue.py +178 -0
@@ 1,178 @@
#!/usr/bin/python3

from typing import List
import random

class Queue:
    def __init__(self):
        self.items = []

    def isEmpty(self):
        return self.items == []

    def enqueue(self, item):
        self.items.append(item)

    def dequeue(self):
        return self.items.pop(0)

    def size(self):
        return len(self.items)

    def __str__(self):
        return str(self.items)


q = Queue()
q.enqueue('hello')
q.enqueue('dog')
q.enqueue(3)
q.dequeue()


def hotPotato(namelist, num):
    simqueue = Queue()
    for name in namelist:
        simqueue.enqueue(name)

    while simqueue.size() > 1:
        for i in range(random.randint(0,10)):
            simqueue.enqueue(simqueue.dequeue())

        simqueue.dequeue()

    return simqueue.dequeue()

# print(
#     hotPotato(
#         ["Bill","David","Susan","Jane","Kent","Brad"],7
#     )
# )


class Printer:
    def __init__(self, ppm):
        self.pagerate = ppm
        self.currentTask = None
        self.timeRemaining = 0

    def tick(self):
        if self.currentTask != None:
            self.timeRemaining = self.timeRemaining - 1
            if self.timeRemaining <= 0:
                self.currentTask = None

    def busy(self):
        if self.currentTask != None:
            return True
        else:
            return False

    def startNext(self,newtask):
        self.currentTask = newtask
        # Divided by 2 because we supposed the average
        # time got reduced
        self.timeRemaining = (newtask.getPages() * 60/self.pagerate) / 2


class Task:
    def __init__(self,time):
        self.timestamp = time
        self.pages = random.randrange(1,21)

    def getStamp(self):
        return self.timestamp

    def getPages(self):
        return self.pages

    def waitTime(self, currenttime):
        return currenttime - self.timestamp


def simulation(numSeconds, pagesPerMinute, numberOfStudents):

    labprinter = Printer(pagesPerMinute)
    printQueue = Queue()
    waitingtimes = []

    for currentSecond in range(numSeconds):

        if newPrintTask(numberOfStudents):
            task = Task(currentSecond)
            printQueue.enqueue(task)

        if (not labprinter.busy()) and (not printQueue.isEmpty()):
            nexttask = printQueue.dequeue()
            waitingtimes.append(nexttask.waitTime(currentSecond))
            labprinter.startNext(nexttask)

        labprinter.tick()

    averageWait=sum(waitingtimes)/len(waitingtimes)
    print("Average Wait %6.2f secs %3d tasks remaining."%(averageWait,printQueue.size()))

def newPrintTask(numberOfStudents):
    # The times 2 is because we assume every student uses
    # the printer twice
    task_average_per_hour = 3600 // (numberOfStudents * 2)
    num = random.randrange(1, task_average_per_hour + 1)
    if num == task_average_per_hour:
        return True
    else:
        return False

for i in range(10):
    simulation(3600,5, 20)



def radix10_sorting(numbers: List[str]) -> List[str]:
    main_bin = Queue()

    bin_0 = Queue()
    bin_1 = Queue()
    bin_2 = Queue()
    bin_3 = Queue()
    bin_4 = Queue()
    bin_5 = Queue()
    bin_6 = Queue()
    bin_7 = Queue()
    bin_8 = Queue()
    bin_9 = Queue()

    digit_bin = {
        "0": bin_0,
        "1": bin_1,
        "2": bin_2,
        "3": bin_3,
        "4": bin_4,
        "5": bin_5,
        "6": bin_6,
        "7": bin_7,
        "8": bin_8,
        "9": bin_9,
    }

    for num in numbers:
        main_bin.enqueue(num)
        reversed_digit = num[::-1]
        for digit in reversed_digit:
            digit_bin[digit].enqueue(main_bin.dequeue())

    # Collect back the value into the main one
    for d_bin in digit_bin.values():
        if d_bin.size() != 0:
            main_bin.enqueue(d_bin.dequeue())

    queued_numbers: List[str] = []
    while main_bin.size() != 0:
        queued_numbers.append(main_bin.dequeue())

    return queued_numbers


#test_numbers = ["667", "534"]
#test_numbers = ["2", "1"]
#test_numbers = ["9", "3", "8", "1"]
print(radix10_sorting(test_numbers))

A  => chapter4/stack.py +292 -0
@@ 1,292 @@
#!/usr/bin/python3

"""
With this structure it is easy
to get the reverse order of the items inserted
"""


class Stack:
    def __init__(self):
        self.items = []

    def isEmpty(self):
        return self.items == []

    def push(self, item):
        self.items.append(item)

    def pop(self):
        return self.items.pop()

    def peek(self):
        return self.items[len(self.items)-1]

    def size(self):
        return len(self.items)

    def __str__(self):
        return str(self.items)


m = Stack()
m.push('x')
m.push('y')
m.push('z')
while not m.isEmpty():
    m.pop()

test_string = "abc"

def revstring(to_reverse: str) -> str:
    reversed_string = ""
    stack = Stack()
    for letter in to_reverse:
        stack.push(letter)
    while not stack.isEmpty():
        reversed_string = reversed_string + stack.peek()
        stack.pop()
    return reversed_string

rev_string = revstring(test_string)
print(rev_string)


def parChecker(symbolString):
    s = Stack()
    balanced = True
    index = 0
    while index < len(symbolString) and balanced:
        symbol = symbolString[index]
        if symbol == "(":
            s.push(symbol)
        else:
            if s.isEmpty():
                balanced = False
            else:
                s.pop()

        index = index + 1

    if balanced and s.isEmpty():
        return True
    else:
        return False

print(parChecker('((()))'))
print(parChecker('(()'))

def parChecker(symbolString):
    s = Stack()
    balanced = True
    index = 0
    while index < len(symbolString) and balanced:
        symbol = symbolString[index]
        if symbol in "([{":
            s.push(symbol)
        else:
            if s.isEmpty():
                balanced = False
            else:
                top = s.pop()
                if not matches(top,symbol):
                       balanced = False
        index = index + 1
    if balanced and s.isEmpty():
        return True
    else:
        return False

def matches(open,close):
    opens = "([{"
    closers = ")]}"
    return opens.index(open) == closers.index(close)


print(parChecker('{({([][])}())}'))
print(parChecker('[{()]'))


def divideBy2(decNumber):
    remstack = Stack()

    while decNumber > 0:
        rem = decNumber % 2
        remstack.push(rem)
        decNumber = decNumber // 2

    binString = ""
    while not remstack.isEmpty():
        binString = binString + str(remstack.pop())

    return binString

print(divideBy2(42))
print(divideBy2(17))
print(divideBy2(45))
print(divideBy2(96))


def baseConverter(decNumber,base):
    digits = "0123456789ABCDEF"

    remstack = Stack()

    while decNumber > 0:
        rem = decNumber % base
        remstack.push(rem)
        decNumber = decNumber // base

    newString = ""
    while not remstack.isEmpty():
        newString = newString + digits[remstack.pop()]

    return newString

#print(baseConverter(25,2))
#print(baseConverter(25,16))


def matches(open,close):
    opens = "<"
    closers = ">"
    return opens.index(open) == closers.index(close)


def html_validator(symbolString):
    opening_angle_bracket = Stack()
    tags = Stack()
    excluded_characters = ["\n", " "]
    balanced = True
    is_tag = True
    current_token = ""
    index = 0
    while index < len(symbolString) and balanced:
        symbol = symbolString[index]
        if symbol in "<":
            is_tag = True
            opening_angle_bracket.push(symbol)
            if symbol not in excluded_characters:
                current_token += symbol
        else:
            if symbol in ">":
                is_tag = False
                if symbol not in excluded_characters:
                    current_token += symbol
                    if tags.size() !=0 and "</" + tags.peek()[1::] == current_token:
                        tags.pop()
                    else:
                        tags.push(current_token)
                    current_token = ""
                top = opening_angle_bracket.pop()
            else:
                if symbol not in excluded_characters and is_tag:
                    current_token += symbol
        index = index + 1
    if balanced and opening_angle_bracket.isEmpty() and tags.isEmpty():
        return True
    else:
        if not opening_angle_bracket.isEmpty():
            print(f"Missing a closing '>' : {opening_angle_bracket.peek()}")
        if not tags.isEmpty():
            tag, tag_type = get_missing_tag(tags)
            print(f"Missing a {tag_type} tag for : {tag}")
        return False


def get_missing_tag(tags: Stack) -> str:
    tag = ""
    tag_type = ""
    while not tag:
        if tags.size() > 0:
            if "</" in tags.peek():
                tags.pop()
            else:
                tag = tags.peek()
        else:
            break
    if "</" in tag:
        tag_type = "opening"
    else:
        tag_type = "closing"
    return tag, tag_type


test_html = """
<html>
      <title>
         Example
      </title>
   </head>

   <body>
      <h1>Hello, world</h1>
   </body>
</html>
"""

print(html_validator(test_html))


def infixToPostfix(infixexpr):
    prec = {}
    prec["*"] = 3
    prec["/"] = 3
    prec["+"] = 2
    prec["-"] = 2
    prec["("] = 1
    opStack = Stack()
    postfixList = []
    tokenList = infixexpr.split()
    breakpoint()
    for token in tokenList:
        if token in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" or token in "0123456789":
            postfixList.append(token)
        elif token == '(':
            opStack.push(token)
        elif token == ')':
            topToken = opStack.pop()
            while topToken != '(':
                postfixList.append(topToken)
                topToken = opStack.pop()
        else:
            while (not opStack.isEmpty()) and \
               (prec[opStack.peek()] >= prec[token]):
                  postfixList.append(opStack.pop())
            opStack.push(token)

    while not opStack.isEmpty():
        postfixList.append(opStack.pop())
    return " ".join(postfixList)

print(infixToPostfix("( A * B ) + ( C * D )"))
#print(infixToPostfix("( A + B ) * C - ( D - E ) * ( F + G )"))
#print(infixToPostfix("( A + B ) * ( C + D ) * ( E + F )"))
#print(infixToPostfix("A + ( ( B + C ) * ( D + E ) )"))
#print(infixToPostfix("A * B * C * D + E + F"))

def postfixEval(postfixExpr):
    operandStack = Stack()
    tokenList = postfixExpr.split()

    for token in tokenList:
        if token in "0123456789":
            operandStack.push(int(token))
        else:
            operand2 = operandStack.pop()
            operand1 = operandStack.pop()
            result = doMath(token,operand1,operand2)
            operandStack.push(result)
    return operandStack.pop()

def doMath(op, op1, op2):
    if op == "*":
        return op1 * op2
    elif op == "/":
        return op1 / op2
    elif op == "+":
        return op1 + op2
    else:
        return op1 - op2

print(postfixEval('7 8 + 3 2 + /'))

A  => chapter5/README.org +27 -0
@@ 1,27 @@
* Recursion

Recursion is a method of solving problems that involves breaking a problem down into smaller and smaller subproblems until you get to a small enough problem that it can be solved trivially. Usually recursion involves a function calling itself. While it may not seem like much on the surface, recursion allows us to write elegant solutions to problems that may otherwise be very difficult to program.


* Stack frames

Each time we make a call to toStr, we push a character on the stack. Returning to the previous example we can see that after the fourth call to toStr the stack would look like Figure 5. Notice that now we can simply pop the characters off the stack and concatenate them into the final result, "1010".


#+DOWNLOADED: screenshot @ 2021-11-05 21:46:34
[[file:Stack_frames/2021-11-05_21-46-34_screenshot.png]]


The previous example gives us some insight into how Python implements a recursive function call. When a function is called in Python, a stack frame is allocated to handle the local variables of the function. When the function returns, the return value is left on top of the stack for the calling function to access. Figure 6 illustrates the call stack after the return statement on line 4.



#+DOWNLOADED: screenshot @ 2021-11-05 21:45:54
[[file:Recursion/2021-11-05_21-45-54_screenshot.png]]


Figure 6: Call Stack Generated from toStr(10,2)

Notice that the call to toStr(2//2,2) leaves a return value of "1" on the stack. This return value is then used in place of the function call (toStr(1,2)) in the expression "1" + convertString[2%2], which will leave the string "10" on the top of the stack. In this way, the Python call stack takes the place of the stack we used explicitly in Listing 4. In our list summing example, you can think of the return value on the stack taking the place of an accumulator variable.

The stack frames also provide a scope for the variables used by the function. Even though we are calling the same function over and over, each call creates a new scope for the variables that are local to the function.

A  => chapter5/Recursion/2021-11-05_21-45-54_screenshot.png +0 -0
A  => chapter5/Stack_frames/2021-11-05_21-46-34_screenshot.png +0 -0
A  => chapter5/base_converter.py +12 -0
@@ 1,12 @@
#!/usr/bin/python3

def toStr(n,base):
   convertString = "0123456789ABCDEF"
   if n < base:
      return convertString[n]
   else:
      return toStr(n//base,base) + convertString[n%base]

print(toStr(10,2))

print(toStr(1453,16))

A  => chapter5/euclid.py +26 -0
@@ 1,26 @@
#!/usr/bin/python3

def euclid(m: int, n: int) -> int:
    remainder  = m % n
    while remainder != 0:
        remainder  = m % n
        if remainder == 0:
            return n
        m, n = n, remainder
    else:
        return n


def euclid_improved(m: int, n: int) -> int:
    remainder  = m % n
    if remainder == 0:
        return n
    else:
        return euclid_improved(n, remainder)


if __name__ == '__main__':
    m = int(input("Entrez un nombre entier positif m : "))
    n = int(input("Entrez un nombre entier positif n : "))
    result = euclid_improved(m, n)
    print(f"Le PGCD entre {m} et {n} est {result}")

A  => chapter5/factorial.py +81 -0
@@ 1,81 @@
import string
from typing import List, Dict

def factorial(n: int) -> int:
    if n <= 1:
        return 1
    else:
        return n * factorial(n - 1)

# print(factorial(5))
# fac(5) -> 120: 5 * 4 * 3 * 2 * 1
# fac(4) -> 24: 4 * 3 * 2 * 1
# fac(3) -> 6 : 3 * 2 * 1
# fac(2) -> 2 : 2 * 1
# fac(1) -> 1 : 1
# fac(0) -> 1


def reverse_list(my_list: List[str | int]) -> List[str | int]:
    if len(my_list) <= 1:
        return my_list
    else:
        return my_list[-1::] + reverse_list(my_list[:-1])


test_list = [10,20,30]
test_list = [1,5,10,20,100,1000,10000]
#print(reverse_list(test_list))


def reverse_string(to_reverse: str) -> str:
    if len(to_reverse) <= 1:
        return to_reverse
    else:
        return to_reverse[-1] + reverse_string(to_reverse[:-1])


test_string = "abc"
#print(reverse_string(test_string))

def is_palindrome(my_string: str) -> bool:
    removed_punctuation = my_string.translate(
        str.maketrans('', '', string.punctuation + '’')
    )
    my_string = ''.join(removed_punctuation.lower().split())
    return bool(my_string == reverse_string(my_string))

test_strings = [
    "kayak",
    "radar",
    "aibohphobia",
    "Live not on evil",
    "Reviled did I live, said I, as evil I did deliver",
    "Go hang a salami; I’m a lasagna hog.",
    "Able was I ere I saw Elba",
    "Kanakanak", # a town in Alaska,
    "Wassamassaw", # a town in South Dakota
    "madam i’m adam",
    "hannah",
    "hello",
    "x",
    ""
]
#for test_string in test_strings:
#    print(is_palindrome(test_string))

# Fibonacci sequence
# The Rule is xn = xn−1 + xn−2 or F_n is the sum of its 2 preceding number

# 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, .

def fibonacci(n: int, memo: Dict[int, int]) -> int:
    if n <= 1:
        return n
    elif n in memo:
        return memo[n]
    else:
        memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo)
    return memo[n]

print(fibonacci(998, {0: 1,1: 1}))

A  => chapter5/recursion.py +162 -0
@@ 1,162 @@
#!/usr/bin/python3

def listsum(numList):
    theSum = 0
    for i in numList:
        theSum = theSum + i
    return theSum

print(listsum([1,3,5,7,9]))


def listsum(numList):
   if len(numList) == 1:
        return numList[0]
   else:
        return numList[0] + listsum(numList[1:])

print(listsum([1,3,5,7,9]))

def toStr(n,base):
   convertString = "0123456789ABCDEF"
   if n < base:
      return convertString[n]
   else:
      return toStr(n//base,base) + convertString[n%base]

print(toStr(1453,16))


def reverse(s):
    return s

testEqual(reverse("hello"),"olleh")
testEqual(reverse("l"),"l")
testEqual(reverse("follow"),"wollof")
testEqual(reverse(""),"")

from test import testEqual
def removeWhite(s):
    return s

def isPal(s):
    return False

testEqual(isPal(removeWhite("x")),True)
testEqual(isPal(removeWhite("radar")),True)
testEqual(isPal(removeWhite("hello")),False)
testEqual(isPal(removeWhite("")),True)
testEqual(isPal(removeWhite("hannah")),True)
testEqual(isPal(removeWhite("madam i'm adam")),True)

rStack = Stack()

def toStr(n,base):
    convertString = "0123456789ABCDEF"
    while n > 0:
        if n < base:
            rStack.push(convertString[n])
        else:
            rStack.push(convertString[n % base])
        n = n // base
    res = ""
    while not rStack.isEmpty():
        res = res + str(rStack.pop())
    return res

print(toStr(1453,16))


import turtle

myTurtle = turtle.Turtle()
myWin = turtle.Screen()

def drawSpiral(myTurtle, lineLen):
    if lineLen > 0:
        myTurtle.forward(lineLen)
        myTurtle.right(90)
        drawSpiral(myTurtle,lineLen-5)

drawSpiral(myTurtle,100)
myWin.exitonclick()

import turtle

def tree(branchLen,t):
    if branchLen > 5:
        t.forward(branchLen)
        t.right(20)
        tree(branchLen-15,t)
        t.left(40)
        tree(branchLen-15,t)
        t.right(20)
        t.backward(branchLen)

def main():
    t = turtle.Turtle()
    myWin = turtle.Screen()
    t.left(90)
    t.up()
    t.backward(100)
    t.down()
    t.color("green")
    tree(75,t)
    myWin.exitonclick()

main()


import turtle

def drawTriangle(points,color,myTurtle):
    myTurtle.fillcolor(color)
    myTurtle.up()
    myTurtle.goto(points[0][0],points[0][1])
    myTurtle.down()
    myTurtle.begin_fill()
    myTurtle.goto(points[1][0],points[1][1])
    myTurtle.goto(points[2][0],points[2][1])
    myTurtle.goto(points[0][0],points[0][1])
    myTurtle.end_fill()

def getMid(p1,p2):
    return ( (p1[0]+p2[0]) / 2, (p1[1] + p2[1]) / 2)

def sierpinski(points,degree,myTurtle):
    colormap = ['blue','red','green','white','yellow',
                'violet','orange']
    drawTriangle(points,colormap[degree],myTurtle)
    if degree > 0:
        sierpinski([points[0],
                        getMid(points[0], points[1]),
                        getMid(points[0], points[2])],
                   degree-1, myTurtle)
        sierpinski([points[1],
                        getMid(points[0], points[1]),
                        getMid(points[1], points[2])],
                   degree-1, myTurtle)
        sierpinski([points[2],
                        getMid(points[2], points[1]),
                        getMid(points[0], points[2])],
                   degree-1, myTurtle)

def main():
   myTurtle = turtle.Turtle()
   myWin = turtle.Screen()
   myPoints = [[-100,-50],[0,100],[100,-50]]
   sierpinski(myPoints,3,myTurtle)
   myWin.exitonclick()

main()

def moveTower(height,fromPole, toPole, withPole):
    if height >= 1:
        moveTower(height-1,fromPole,withPole,toPole)
        moveDisk(fromPole,toPole)
        moveTower(height-1,withPole,toPole,fromPole)

def moveDisk(fp,tp):
    print("moving disk from",fp,"to",tp)

moveTower(3,"A","B","C")

A  => chapter5/stack_frame.py +20 -0
@@ 1,20 @@
#!/usr/bin/python3

from pythonds.basic import Stack

rStack = Stack()

def toStr(n,base):
    convertString = "0123456789ABCDEF"
    while n > 0:
        if n < base:
            rStack.push(convertString[n])
        else:
            rStack.push(convertString[n % base])
        n = n // base
    res = ""
    while not rStack.isEmpty():
        res = res + str(rStack.pop())
    return res

print(toStr(1453,16))

A  => chapter5/tree.py +39 -0
@@ 1,39 @@
#!/usr/bin/python3

import turtle

#myTurtle = turtle.Turtle()
#myWin = turtle.Screen()

def drawSpiral(myTurtle, lineLen):
    if lineLen > 0:
        myTurtle.forward(lineLen)
        myTurtle.right(90)
        drawSpiral(myTurtle,lineLen-5)

#drawSpiral(myTurtle,100)
#myWin.exitonclick()

def tree(branchLen,t):
    if branchLen > 5:
        t.forward(branchLen)
        t.right(20)
        tree(branchLen-15,t)
        breakpoint()
        t.left(40)
        tree(branchLen-15,t)
        t.right(20)
        t.backward(branchLen)

def main():
    t = turtle.Turtle()
    myWin = turtle.Screen()
    t.left(90)
    t.up()
    t.backward(100)
    t.down()
    t.color("green")
    tree(30,t)
    myWin.exitonclick()

main()

A  => chapter6/README.org +7 -0
@@ 1,7 @@
* TODO

- Heapsort

- Prove that heapsort is o(n log n)

- Optimal value for the hash resize, resize when the hash table is full at 50 75 % ? why ?

A  => chapter6/binary_search.py +75 -0
@@ 1,75 @@
#!/usr/bin/python3

import random

# def binarySearch(alist, item):
#     first = 0
#     last = len(alist)-1
#     found = False

#     while first<=last and not found:
#         midpoint = (first + last)//2
#         if alist[midpoint] == item:
#             found = True
#         else:
#             if item < alist[midpoint]:
#                 last = midpoint-1
#             else:
#                 first = midpoint+1

#     return found

#testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42]
#print(binarySearch(testlist, 3))
#print(binarySearch(testlist, 13))


def binarySearch(alist, item):
    if len(alist) == 0:
        return False
    else:
        midpoint = len(alist)//2
        if alist[midpoint]==item:
          return True
        else:
          if item<alist[midpoint]:
            return binarySearch(alist[:midpoint],item)
          else:
            return binarySearch(alist[midpoint+1:],item)

#testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,]
testlist = [3, 5, 6, 8, 11, 12, 14, 15, 17, 18]
breakpoint()
print(binarySearch(testlist, 16))
#print(binarySearch(testlist, 3))
#print(binarySearch(testlist, 13))

# The same but without the slice

def binarySearch(alist, first, last, item):
    midpoint = (first + last) // 2
    if first > last and item != alist[midpoint]:
        return False
    else:
        if alist[midpoint] == item:
            return True
        else:
            if item < alist[midpoint]:
                return binarySearch(alist, first, midpoint - 1, item)
            else:
                return binarySearch(alist, midpoint + 1, last, item)

#testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,]
#testlist = [17, 46, 23, 56, 89, 86, 87, 16, 87, 65]
# print(binarySearch(testlist, 0, len(testlist) - 1,  3))
# print(binarySearch(testlist, 0, len(testlist) - 1, 13))
# print(binarySearch(testlist, 0, len(testlist) - 1, 46))
# [random.randint(10,100) for x in range(random.randint(5,10))]


# When we split the list enough times, we end up with a list that has
# just one item. Either that is the item we are looking for or it is
# not. Either way, we are done. The number of comparisons necessary to
# get to this point is i where n2i=1. Solving for i gives us i=logn. The
# maximum number of comparisons is logarithmic with respect to the
# number of items in the list. Therefore, the binary search is O(logn).

A  => chapter6/bubble_sort.py +71 -0
@@ 1,71 @@
#!/usr/bin/python3

# The bubble sort makes multiple passes through a list. It compares
# adjacent items and exchanges those that are out of order. Each pass
# through the list places the next largest value in its proper place. In
# essence, each item “bubbles” up to the location where it belongs.

def bubbleSort(alist):
    for passnum in range(len(alist) - 1, 0, -1):
        for i in range(passnum):
            if alist[i] > alist[i+1]:
                alist[i+1], alist[i] = alist[i], alist[i+1]

alist = [54,26,93,17,77,31,44,55,20]
alist = [2,3,4,5,1]
bubbleSort(alist)
print(alist)

# A bubble sort is often considered the most inefficient sorting method
# since it must exchange items before the final location is known. These
# “wasted” exchange operations are very costly. However, because the
# bubble sort makes passes through the entire unsorted portion of the
# list, it has the capability to do something most sorting algorithms
# cannot. In particular, if during a pass there are no exchanges, then
# we know that the list must be sorted. A bubble sort can be modified to
# stop early if it finds that the list has become sorted. This means
# that for lists that require just a few passes, a bubble sort may have
# an advantage in that it will recognize the sorted list and
# stop. ActiveCode 2 shows this modification, which is often referred to
# as the short bubble.

def shortBubbleSort(alist):
    exchanges = True
    passnum = len(alist)-1
    while passnum > 0 and exchanges:
       exchanges = False
       for i in range(passnum):
           if alist[i]>alist[i+1]:
               exchanges = True
               temp = alist[i]
               alist[i] = alist[i+1]
               alist[i+1] = temp
       passnum = passnum-1

# alist=[20,30,40,90,50,60,70,80,100,110]
# shortBubbleSort(alist)
# print(alist)

# Alternative sort, this one makes less pass if there is like only one
# item to move backwards

def bubbleSortAlternating(alist):
    for passnum in range(len(alist) -1, 0, -1):
        if passnum % 2 == 0:
            for i in range(passnum):
                if alist[i] > alist[i+1]:
                    alist[i+1], alist[i] = alist[i], alist[i+1]
        else:
            for i in reversed(range(passnum)):
                if alist[i] > alist[i+1]:
                    alist[i+1], alist[i] = alist[i], alist[i+1]
    print(passnum)

alist = [54,26,93,17,77,31,44,55,20]
alist = [2,3,4,5,1]
bubbleSortAlternating(alist)
print(alist)

# (2,3,4,5,1)
# (2,3,4,1,5)
# (1,2,3,4,5)

A  => chapter6/hash.py +52 -0
@@ 1,52 @@
#!/usr/bin/python3

# Lambda (load factor) = number of items/ tablesize

# 10% full
# 25% full
# 50% full
# 75% full
# 90% full
# 99% full

test_values = {0.10, 0.25, 0.50, 0.75, 0.90, 0.99}


def succesful_probing(n: int) -> float:
    avg_comparison = 1/2 * (1 + (1/1-n))
    return avg_comparison


def unsuccesful_probing(n: int) -> float:
    avg_comparison = 1/2 * (1 + (1/1-n) ** 2)
    return avg_comparison


def succesful_chaining(n: int) -> float:
    avg_comparison = 1 + n/2
    return avg_comparison


def unsuccesful_chaining(n: int) -> float:
    avg_comparison = n
    return avg_comparison


print("Succesful probing")
for count, n in enumerate(test_values, 1):
    print(count, ": ",  "load factor is : ", n,   "average number of compaisons :",succesful_probing(n))


print("Unsuccesful probing")
for count, n in enumerate(test_values, 1):
    print(count, ": ",  "load factor is : ", n,   "average number of compaisons :",unsuccesful_probing(n))


print("Succesful chaining")
for count, n in enumerate(test_values, 1):
    print(count, ": ",  "load factor is : ", n,   "average number of compaisons :",succesful_chaining(n))


print("Unsuccesful chaining")
for count, n in enumerate(test_values, 1):
    print(count, ": ", "load factor is : ", n,  "average number of compaisons :", unsuccesful_chaining(n))

A  => chapter6/hash_string.py +9 -0
@@ 1,9 @@
#!/usr/bin/python3

def hash(astring, tablesize):
    sum = 0
    for pos in range(len(astring)):
        sum = sum + ord(astring[pos])

    return sum%tablesize


A  => chapter6/insertion_sort.py +24 -0
@@ 1,24 @@
#!/usr/bin/python3

# We begin by assuming that a list with one item (position 0) is already
# sorted. On each pass,  one for each item 1 through n−1,  the current
# item is checked against those in the already sorted sublist. As we
# look back into the already sorted sublist,  we shift those items that
# are greater to the right. When we reach a smaller item or the end of
# the sublist,  the current item can be inserted.

def insertionSort(alist):
   for index in range(1,  len(alist)):

     currentvalue = alist[index]
     position = index

     while position > 0 and alist[position-1] > currentvalue:
         alist[position] = alist[position-1]
         position = position-1

     alist[position] = currentvalue

alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
insertionSort(alist)
print(alist)

A  => chapter6/map.py +217 -0
@@ 1,217 @@
#!/usr/bin/python3

class HashTable:
    def __init__(self):
        self.size = 11
        self.slots = [None] * self.size
        self.data = [None] * self.size
        self.load = 0
        self.load_factor = 0

    def put(self, key, data):
        # Check the capacity of the map
        if self.load_factor >= 75:
            growth_factor = 10
            self.size = self.size + growth_factor
            # Need to recalculate the hash for the elements that were in the slots
            old_slots = self.slots
            old_datas = self.data
            self.slots = [None] * self.size
            self.data = [None] * self.size
            self.load = 0
            self.load_factor = int((self.load / self.size) * 100)
            for old_slot, old_data in zip(old_slots, old_datas):
                if old_slot is not None:
                    self.put(old_slot, old_data)
 
        hashvalue = self.hashfunction(key, len(self.slots))

        if self.slots[hashvalue] is None:
            self.slots[hashvalue] = key
            self.data[hashvalue] = data
            self.load += 1
            self.load_factor = int((self.load / self.size) * 100)
        else:
            if self.slots[hashvalue] == key:
                self.data[hashvalue] = data  #replace
            else:
                nextslot = self.rehash(hashvalue,len(self.slots))
                while self.slots[nextslot] is not None and \
                      self.slots[nextslot] != key:
                    nextslot = self.rehash(nextslot, len(self.slots))
                if self.slots[nextslot] is None:
                    self.slots[nextslot] = key
                    self.data[nextslot] = data
                    self.load += 1
                    self.load_factor = int((self.load / self.size) * 100)
                else:
                    self.data[nextslot] = data #replace

    def hashfunction(self, key, size):
         return key % size

    def rehash(self, oldhash, size):
        return (oldhash+1) % size

    def get(self, key):
      startslot = self.hashfunction(key, len(self.slots))

      data = None
      stop = False
      found = False
      position = startslot
      while self.slots[position] != None and  \
                           not found and not stop:
         if self.slots[position] == key:
           found = True
           data = self.data[position]
         else:
           position=self.rehash(position,len(self.slots))
           if position == startslot:
               stop = True
      return data

    def __getitem__(self,key):
        return self.get(key)

    def __setitem__(self,key,data):
        self.put(key,data)

    def __len__(self):
        return len([x for x in self.slots if x is not None])

    def __contains__(self, key):
        # Could also use a try except indexoutofbonds?
        # that's something that is done for iteration with StopIteration
        return key in self.slots

    def __delitem__(self, key):
        startslot = self.hashfunction(key, len(self.slots))
        stop = False
        found = False
        position = startslot
        while self.slots[position] != None and  \
                            not found and not stop:
            if self.slots[position] == key:
                found = True
            else:
                position=self.rehash(position,len(self.slots))
                if position == startslot:
                    stop = True
        del self.slots[position]
        del self.data[position]
        self.load -= 1
        self.load_factor = int((self.load / self.size) * 100)


H=HashTable()
H[54]="cat"
H[26]="dog"
H[93]="lion"
H[17]="tiger"
H[77]="bird"
H[31]="cow"
H[44]="goat"
H[55]="pig"
H[20]="chicken"
print(H.slots)
print(H.data)

print(H[20])

print(H[17])
H[20]='duck'
print(H[20])
print(H[99])
print(len(H))
print(20 in H)

# Filling to capacity
H[104]="Creeper"
H[158]="chicken"
H[24]="turtle"
H[25]="eagle"
H[15]="blue eyes white dragon"
print(H.slots)
print(H.data)
print(H[104])
print(len(H))
del H[104]


class HashTableQuadratic(HashTable):
    def __init__(self):
        super().__init__()
        self.quadratic = 1

    def put(self, key, data):
        # Check the capacity of the map
        if self.load_factor >= 75:
            growth_factor = 10
            self.size = self.size + growth_factor
            # Need to recalculate the hash for the elements that were in the slots
            old_slots = self.slots
            old_datas = self.data
            self.slots = [None] * self.size
            self.data = [None] * self.size
            self.load = 0
            self.load_factor = int((self.load / self.size) * 100)
            for old_slot, old_data in zip(old_slots, old_datas):
                if old_slot is not None:
                    self.put(old_slot, old_data)
 
        hashvalue = self.hashfunction(key, len(self.slots))

        if self.slots[hashvalue] is None:
            self.slots[hashvalue] = key
            self.data[hashvalue] = data
            self.load += 1
            self.load_factor = int((self.load / self.size) * 100)
        else:
            if self.slots[hashvalue] == key:
                self.data[hashvalue] = data  #replace
            else:
                self.quadratic = 1
                nextslot = self.rehash(hashvalue,len(self.slots))
                while self.slots[nextslot] is not None and \
                      self.slots[nextslot] != key:
                    nextslot = self.rehash(nextslot, len(self.slots))
                if self.slots[nextslot] is None:
                    self.slots[nextslot] = key
                    self.data[nextslot] = data
                    self.load += 1
                    self.load_factor = int((self.load / self.size) * 100)
                else:
                    self.data[nextslot] = data #replace

    def rehash(self, oldhash, size):
        newhash = oldhash + self.quadratic ** self.quadratic
        self.quadratic += 1
        if newhash > self.size - 1:
            return newhash % self.size
        return newhash

J = HashTableQuadratic()
J[54]="cat"
J[26]="dog"
J[93]="lion"
J[17]="tiger"
J[77]="bird"
J[31]="cow"
J[44]="goat"
J[55]="pig"
J[20]="chicken"

print(J.slots)
print(J.data)
print(len(J))

#J[545]="pig"
#J[207]="chicken"
#
#
#print(J.slots)
#print(J.data)
#print(len(J))
#
#(54,26,93,17,77,31,44,55,20)

A  => chapter6/merge_sort.py +38 -0
@@ 1,38 @@
#!/usr/bin/python3

def mergeSort(alist):
    print("Splitting ",alist)
    if len(alist)>1:
        mid = len(alist)//2
        lefthalf = alist[:mid]
        righthalf = alist[mid:]

        mergeSort(lefthalf)
        mergeSort(righthalf)

        i=0
        j=0
        k=0
        while i < len(lefthalf) and j < len(righthalf):
            if lefthalf[i] <= righthalf[j]:
                alist[k]=lefthalf[i]
                i=i+1
            else:
                alist[k]=righthalf[j]
                j=j+1
            k=k+1

        while i < len(lefthalf):
            alist[k]=lefthalf[i]
            i=i+1
            k=k+1

        while j < len(righthalf):
            alist[k]=righthalf[j]
            j=j+1
            k=k+1
    print("Merging ",alist)

alist = [54,26,93,17,77,31,44,55,20]
mergeSort(alist)
print(alist)

A  => chapter6/quicksort.py +56 -0
@@ 1,56 @@
#!/usr/bin/python3

from typing import List

def quickSort(alist):
   quickSortHelper(alist, 0, len(alist)-1)

def quickSortHelper(alist, first, last):
   if first < last:
      splitpoint = partition(alist, first, last)

      quickSortHelper(alist, first, splitpoint-1)
      quickSortHelper(alist, splitpoint+1, last)


def partition(alist, first, last):
   pivotvalue = alist[first]

   leftmark = first+1
#   leftmark = len(alist) // 2
   rightmark = last

   done = False
   while not done:

       while leftmark <= rightmark and alist[leftmark] <= pivotvalue:
           leftmark = leftmark + 1

       while alist[rightmark] >= pivotvalue and rightmark >= leftmark:
           rightmark = rightmark -1

       if rightmark < leftmark:
           done = True
       else:
           temp = alist[leftmark]
           alist[leftmark] = alist[rightmark]
           alist[rightmark] = temp

   #temp = alist[]
   temp = alist[first]
   alist[first] = alist[rightmark]
   #alist[first] = alist[rightmark]
   alist[rightmark] = temp

   return rightmark

def median_of_three(alist: List[int]) -> int:
   return 1

#alist = [54, 26]
#alist = [17, 20, 26, 31, 44, 54, 55, 77, 93]
#alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
alist = [14, 17, 13, 15, 19, 10, 3, 16, 9, 12]
breakpoint()
quickSort(alist)
print(alist)

A  => chapter6/selection_sort.py +31 -0
@@ 1,31 @@
#!/usr/bin/python3

# The selection sort improves on the bubble sort by making only one
# exchange for every pass through the list. In order to do this, a
# selection sort looks for the largest value as it makes a pass and,
# after completing the pass, places it in the proper location. As with a
# bubble sort, after the first pass, the largest item is in the correct
# place. After the second pass, the next largest is in place. This
# process continues and requires n−1 passes to sort n items, since the
# final item must be in place after the (n−1)st pass.

# Figure 3 shows the entire sorting process. On each pass, the largest
# remaining item is selected and then placed in its proper location. The
# first pass places 93, the second pass places 77, the third places 55,
# and so on. The function is shown in ActiveCode 1.

def selectionSort(alist):
   for fillslot in range(len(alist) - 1, 0, -1):
      positionOfMax = 0
      for location in range(1, fillslot+1):
          if alist[location] > alist[positionOfMax]:
              positionOfMax = location

      alist[fillslot], alist[positionOfMax] = (
          alist[positionOfMax], alist[fillslot]
       )

alist = [54,26,93,17,77,31,44,55,20]
breakpoint()
selectionSort(alist)
print(alist)

A  => chapter6/sequential_search.py +49 -0
@@ 1,49 @@
#!/usr/bin/python3

def sequentialSearch(alist, item):
    pos = 0
    found = False

    while pos < len(alist) and not found:
        if alist[pos] == item:
            found = True
        else:
            pos = pos+1

    return found

testlist = [1, 2, 32, 8, 17, 19, 42, 13, 0]
print(sequentialSearch(testlist, 3))
print(sequentialSearch(testlist, 13))
print(sequentialSearch("abc", "z"))

# Complexity o(n), case where item is present and not present, avg case

def orderedSequentialSearch(alist, item):
    pos = 0
    found = False
    stop = False
    while pos < len(alist) and not found and not stop:
        if alist[pos] == item:
            found = True
        else:
            if alist[pos] > item:
                stop = True
            else:
                pos = pos+1

    return found

testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,]
print(orderedSequentialSearch(testlist, 3))
print(orderedSequentialSearch(testlist, 13))

# With this one there is less comparison to do if the item is not present
# e.g, for a list of integer

# summarizes these results. Note that in the best case we might discover
# that the item is not in the list by looking at only one item. On
# average, we will know after looking through only n2 items. However,
# this technique is still O(n). In summary, a sequential search is
# improved by ordering the list only in the case where we do not find
# the item.

A  => chapter6/shell_sort.py +28 -0
@@ 1,28 @@
#!/usr/bin/python3

def shellSort(alist):
    sublistcount = len(alist)//2
    while sublistcount > 0:

      for startposition in range(sublistcount):
        gapInsertionSort(alist, startposition, sublistcount)

      print("After increments of size", sublistcount, "The list is", alist)

      sublistcount = sublistcount // 2

def gapInsertionSort(alist, start, gap):
    for i in range(start+gap, len(alist), gap):

        currentvalue = alist[i]
        position = i

        while position >= gap and alist[position-gap] > currentvalue:
            alist[position] = alist[position-gap]
            position = position-gap

        alist[position] = currentvalue

alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
shellSort(alist)
print(alist)

A  => chapter7/README.org +3 -0
@@ 1,3 @@
* Chapter 7

- Do the priority Queue

A  => chapter7/avl.py +60 -0
@@ 1,60 @@
#!/usr/bin/python3

def _put(self,key,val,currentNode):
    if key < currentNode.key:
        if currentNode.hasLeftChild():
                self._put(key,val,currentNode.leftChild)
        else:
                currentNode.leftChild = TreeNode(key,val,parent=currentNode)
                self.updateBalance(currentNode.leftChild)
    else:
        if currentNode.hasRightChild():
                self._put(key,val,currentNode.rightChild)
        else:
                currentNode.rightChild = TreeNode(key,val,parent=currentNode)
                self.updateBalance(currentNode.rightChild)

def updateBalance(self,node):
    if node.balanceFactor > 1 or node.balanceFactor < -1:
        self.rebalance(node)
        return
    if node.parent != None:
        if node.isLeftChild():
                node.parent.balanceFactor += 1
        elif node.isRightChild():
                node.parent.balanceFactor -= 1

        if node.parent.balanceFactor != 0:
                self.updateBalance(node.parent)

def rotateLeft(self,rotRoot):
    newRoot = rotRoot.rightChild
    rotRoot.rightChild = newRoot.leftChild
    if newRoot.leftChild != None:
        newRoot.leftChild.parent = rotRoot
    newRoot.parent = rotRoot.parent
    if rotRoot.isRoot():
        self.root = newRoot
    else:
        if rotRoot.isLeftChild():
                rotRoot.parent.leftChild = newRoot
        else:
            rotRoot.parent.rightChild = newRoot
    newRoot.leftChild = rotRoot
    rotRoot.parent = newRoot
    rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor, 0)
    newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor, 0)

def rebalance(self,node):
  if node.balanceFactor < 0:
         if node.rightChild.balanceFactor > 0:
            self.rotateRight(node.rightChild)
            self.rotateLeft(node)
         else:
            self.rotateLeft(node)
  elif node.balanceFactor > 0:
         if node.leftChild.balanceFactor < 0:
            self.rotateLeft(node.leftChild)
            self.rotateRight(node)
         else:
            self.rotateRight(node)

A  => chapter7/binary_heap.py +221 -0
@@ 1,221 @@
#!/usr/bin/python3

"""Min heap implementation to starts with."""

class BinHeap:
    def __init__(self):
        self.heapList = [0]
        self.currentSize = 0
        self.heapSize = 3

    def percUp(self,i):
        while i // 2 > 0:
            if self.heapList[i] < self.heapList[i // 2]:
                tmp = self.heapList[i // 2]
                self.heapList[i // 2] = self.heapList[i]
                self.heapList[i] = tmp
            i = i // 2

    def insert(self,k):
        self.heapList.append(k)
        self.currentSize = self.currentSize + 1
        self.percUp(self.currentSize)

    def percDown(self,i):
        while (i * 2) <= self.currentSize:
            mc = self.minChild(i)
            if self.heapList[i] > self.heapList[mc]:
                tmp = self.heapList[i]
                self.heapList[i] = self.heapList[mc]
                self.heapList[mc] = tmp
            i = mc

    def minChild(self,i):
        if i * 2 + 1 > self.currentSize:
            return i * 2
        else:
            if self.heapList[i*2] < self.heapList[i*2+1]:
                return i * 2
            else:
                return i * 2 + 1

    def delMin(self):
        retval = self.heapList[1]
        self.heapList[1] = self.heapList[self.currentSize]
        self.currentSize = self.currentSize - 1
        self.heapList.pop()
        self.percDown(1)
        return retval

    def findMin(self):
        return self.heapList[1]

    def isEmpty(self):
        return bool(len(self.heapList) <= 1)

    def size(self):
        return len(self.heapList[1:])

    def buildHeap(self,alist):
        i = len(alist) // 2
        self.currentSize = len(alist)
        self.heapList = [0] + alist[:]
        while (i > 0):
            self.percDown(i)
            i = i - 1

    def __str__(self):
        return str(self.heapList)

bh = BinHeap()
#bh.buildHeap(
#    [5, 9, 11, 14, 18, 19, 21, 33, 17, 27]
#)
bh.buildHeap([9,5,6,2,3])
print(bh.heapList)

# print(bh.delMin())
# print(bh.delMin())
# print(bh.delMin())
# print(bh.delMin())
# print(bh.delMin())

# bh.insert(5)
# bh.insert(9)
# bh.insert(11)
# bh.insert(14)
# bh.insert(18)
# bh.insert(19)
# bh.insert(21)
# bh.insert(33)
# bh.insert(17)
# bh.insert(27)

# print(bh.delMin())
# print(bh.delMin())
# print(bh.delMin())
# print(bh.delMin())
# print(bh.delMin())
# print(bh.delMin())
# print(bh.delMin())
# print(bh.delMin())
# print(bh.delMin())
# print(bh.delMin())


# Max heap

# Should get 9,6,5,3,2

class maxHeap(BinHeap):

    def findMax(self):
        return self.heapList[1]

    def percUp(self,i):
        while i // 2 > 0:
            if self.heapList[i] >= self.heapList[i // 2]:
                tmp = self.heapList[i // 2]
                self.heapList[i // 2] = self.heapList[i]
                self.heapList[i] = tmp
            i = i // 2

    def insert(self,k):
        self.heapList.append(k)
        self.currentSize = self.currentSize + 1
        self.percUp(self.currentSize)

    def delMax(self):
        retval = self.heapList[1]
        self.heapList[1] = self.heapList[self.currentSize // 2]
        self.currentSize = self.currentSize - 1
        self.heapList.pop(1)
        i = 2 if self.currentSize % 2 == 0 and self.currentSize > 0 else 1
        self.percUp(i)
        return retval

    def maxChild(self,i):
        if i * 2 + 1 > self.currentSize:
            return i * 2
        else:
            if self.heapList[i*2] >= self.heapList[i*2+1]:
                return i * 2
            else:
                return i * 2 + 1

    def percDown(self,i):
        while (i * 2) <= self.currentSize:
            mc = self.maxChild(i)
            if self.heapList[i] <= self.heapList[mc]:
                tmp = self.heapList[i]
                self.heapList[i] = self.heapList[mc]
                self.heapList[mc] = tmp
            i = mc

    def buildHeap(self,alist):
        i = len(alist) // 2
        self.currentSize = len(alist)
        self.heapList = [0] + alist
        while (i > 0):
            self.percDown(i)
            i = i - 1

mh = maxHeap()
breakpoint()
mh.buildHeap(
       [5, 9, 11, 14, 18, 19, 21, 33, 17, 27]
)
#mh.buildHeap([9,5,6,2,3])
print(mh.heapList)
# # mh.insert(9)
# # mh.insert(5)
# # mh.insert(6)
# # mh.insert(2)
# # mh.insert(3)

# mh.insert(5)
# mh.insert(9)
# mh.insert(11)
# mh.insert(14)
# mh.insert(18)
# mh.insert(19)
# mh.insert(21)
# mh.insert(33)
# mh.insert(17)
# mh.insert(27)

print(mh.delMax())
print(mh.delMax())
print(mh.delMax())
print(mh.delMax())
print(mh.delMax())
print(mh.delMax())
print(mh.delMax())
print(mh.delMax())
print(mh.delMax())
print(mh.delMax())



class PriorityQueue(BinHeap):

    def enqueue(self, value):
        self.insert(value)

    def dequeue(self):
        return self.heapList.pop(1)


# priority_queue = PriorityQueue()
# priority_queue.enqueue(9)
# priority_queue.enqueue(1)
# priority_queue.enqueue(8)
# priority_queue.enqueue(2)

# print(priority_queue.dequeue())
# print(priority_queue.dequeue())


class heapSort():
    """Using the buildHeap method, write a sorting function that can sort a list in O(n log(n)) time."""
    pass

A  => chapter7/binary_search_tree.py +241 -0
@@ 1,241 @@
#!/usr/bin/python3

class TreeNode:
    def __init__(self,key,val,left=None,right=None,parent=None):
        self.key = key
        self.payload = val
        self.leftChild = left
        self.rightChild = right
        self.parent = parent

    def hasLeftChild(self):
        return self.leftChild

    def hasRightChild(self):
        return self.rightChild

    def isLeftChild(self):
        return self.parent and self.parent.leftChild == self

    def isRightChild(self):
        return self.parent and self.parent.rightChild == self

    def isRoot(self):
        return not self.parent

    def isLeaf(self):
        return not (self.rightChild or self.leftChild)

    def hasAnyChildren(self):
        return self.rightChild or self.leftChild

    def hasBothChildren(self):
        return self.rightChild and self.leftChild

    def spliceOut(self):
        if self.isLeaf():
            if self.isLeftChild():
                self.parent.leftChild = None
            else:
                self.parent.rightChild = None
        elif self.hasAnyChildren():
            if self.hasLeftChild():
                if self.isLeftChild():
                    self.parent.leftChild = self.leftChild
                else:
                    self.parent.rightChild = self.leftChild
                self.leftChild.parent = self.parent
            else:
                if self.isLeftChild():
                    self.parent.leftChild = self.rightChild
                else:
                    self.parent.rightChild = self.rightChild
                self.rightChild.parent = self.parent

    def findSuccessor(self):
        succ = None
        if self.hasRightChild():
            succ = self.rightChild.findMin()
        else:
            if self.parent:
                   if self.isLeftChild():
                       succ = self.parent
                   else:
                       self.parent.rightChild = None
                       succ = self.parent.findSuccessor()
                       self.parent.rightChild = self
        return succ

    def findMin(self):
        current = self
        while current.hasLeftChild():
            current = current.leftChild
        return current

    def replaceNodeData(self,key,value,lc,rc):
        self.key = key
        self.payload = value
        self.leftChild = lc
        self.rightChild = rc
        if self.hasLeftChild():
            self.leftChild.parent = self
        if self.hasRightChild():
            self.rightChild.parent = self

    def __iter__(self):
       if self:
          if self.hasLeftChild():
                 for elem in self.leftChiLd:
                    yield elem
          yield self.key
          if self.hasRightChild():
                 for elem in self.rightChild:
                    yield elem



class BinarySearchTree:

    def __init__(self):
        self.root = None
        self.size = 0

    def length(self):
        return self.size

    def __len__(self):
        return self.size

    def put(self,key,val):
        if self.root:
            self._put(key,val,self.root)
        else:
            self.root = TreeNode(key,val)
        self.size = self.size + 1

    def _put(self,key,val,currentNode):
        if key < currentNode.key:
            if currentNode.hasLeftChild():
                   self._put(key,val,currentNode.leftChild)
            else:
                   currentNode.leftChild = TreeNode(key,val,parent=currentNode)
        elif key == currentNode.key:
            currentNode.payload = val
        else:
            if currentNode.hasRightChild():
                   self._put(key,val,currentNode.rightChild)
            else:
                   currentNode.rightChild = TreeNode(key,val,parent=currentNode)

    def __setitem__(self,k,v):
       self.put(k,v)

    def get(self,key):
       if self.root:
           res = self._get(key,self.root)
           if res:
                  return res.payload
           else:
                  return None
       else:
           return None

    def _get(self,key,currentNode):
       if not currentNode:
           return None
       elif currentNode.key == key:
           return currentNode
       elif key < currentNode.key:
           return self._get(key,currentNode.leftChild)
       else:
           return self._get(key,currentNode.rightChild)

    def __getitem__(self,key):
       return self.get(key)

    def __contains__(self,key):
       if self._get(key,self.root):
           return True
       else:
           return False

    def delete(self,key):
      if self.size > 1:
         nodeToRemove = self._get(key,self.root)
         if nodeToRemove:
             self.remove(nodeToRemove)
             self.size = self.size-1
         else:
             raise KeyError('Error, key not in tree')
      elif self.size == 1 and self.root.key == key:
         self.root = None
         self.size = self.size - 1
      else:
         raise KeyError('Error, key not in tree')

    def __delitem__(self,key):
       self.delete(key)

    def remove(self,currentNode):
         if currentNode.isLeaf(): #leaf
           if currentNode == currentNode.parent.leftChild:
               currentNode.parent.leftChild = None
           else:
               currentNode.parent.rightChild = None
         elif currentNode.hasBothChildren(): #interior
           succ = currentNode.findSuccessor()
           succ.spliceOut()
           currentNode.key = succ.key
           currentNode.payload = succ.payload

         else: # this node has one child
           if currentNode.hasLeftChild():
             if currentNode.isLeftChild():
                 currentNode.leftChild.parent = currentNode.parent
                 currentNode.parent.leftChild = currentNode.leftChild
             elif currentNode.isRightChild():
                 currentNode.leftChild.parent = currentNode.parent
                 currentNode.parent.rightChild = currentNode.leftChild
             else:
                 currentNode.replaceNodeData(currentNode.leftChild.key,
                                    currentNode.leftChild.payload,
                                    currentNode.leftChild.leftChild,
                                    currentNode.leftChild.rightChild)
           else:
             if currentNode.isLeftChild():
                 currentNode.rightChild.parent = currentNode.parent
                 currentNode.parent.leftChild = currentNode.rightChild
             elif currentNode.isRightChild():
                 currentNode.rightChild.parent = currentNode.parent
                 currentNode.parent.rightChild = currentNode.rightChild
             else:
                 currentNode.replaceNodeData(currentNode.rightChild.key,
                                    currentNode.rightChild.payload,
                                    currentNode.rightChild.leftChild,
                                    currentNode.rightChild.rightChild)




mytree = BinarySearchTree()
mytree[3]="red"
mytree[4]="blue"
mytree[6]="yellow"
mytree[2]="at"

print(mytree[6])
print(mytree[2])
print(mytree[3])
print(mytree[4])

print("----------------")

# Overriding the second slot

mytree[2]="ataz"

print(mytree[6])
print(mytree[2])
print(mytree[3])
print(mytree[4])


A  => chapter7/parse_tree.py +94 -0
@@ 1,94 @@
#!/usr/bin/python3

import operator
from pythonds.basic import Stack
from pythonds.trees import BinaryTree


def buildParseTree(fpexp: str) -> BinaryTree:
    fplist = []
    for token in fpexp:
        is_last_token_number = False
        is_cur_token_number = False
        try:
            is_cur_token_number = isinstance(int(token), int)
        except ValueError:
            pass
        try:
            if len(fplist) > 0:
                is_last_token_number = bool(int(fplist[-1]))
        except ValueError:
            pass
        if token == ' ':
            continue
        elif len(fplist) > 0 and is_last_token_number and is_cur_token_number:
            fplist[-1] = fplist[-1] + token
        else:
            fplist.append(token)

    pStack = Stack()
    eTree = BinaryTree('')
    pStack.push(eTree)
    currentTree = eTree

    for i in fplist:
        if i == '(':
            currentTree.insertLeft('')
            pStack.push(currentTree)
            currentTree = currentTree.getLeftChild()

        elif i in ['+', '-', '*', '/']:
            currentTree.setRootVal(i)
            currentTree.insertRight('')
            pStack.push(currentTree)
            currentTree = currentTree.getRightChild()

        elif i == ')':
            currentTree = pStack.pop()

        elif i not in ['+', '-', '*', '/', ')']:
            try:
                currentTree.setRootVal(int(i))
                parent = pStack.pop()
                currentTree = parent

            except ValueError:
                raise ValueError("token '{}' is not a valid integer".format(i))

    return eTree


pt = buildParseTree("( ( 10 + 5 ) * 3 )")
pt2 = buildParseTree("((10+5)*3)")
pt3 = buildParseTree("((14897+4874)*3)")
pt.postorder()  # defined and explained in the next section

def evaluate(parseTree: BinaryTree):
    """Evaluate a parse tree."""
    opers = {
        '+': operator.add,
        '-': operator.sub,
        '*': operator.mul,
        '/': operator.truediv,
        ' ': operator.or_,
        ' ': operator.and_,
        ' ': operator.is_not,
    }

    leftC = parseTree.getLeftChild()
    rightC = parseTree.getRightChild()

    if leftC and rightC:
        fn = opers[parseTree.getRootVal()]
        return fn(evaluate(leftC), evaluate(rightC))
    else:
        return parseTree.getRootVal()


print(evaluate(pt))
print(evaluate(pt2))
print(evaluate(pt3))


def calc_derivative(parseTree: BinaryTree) -> int:
    pass

A  => chapter7/tree_aslist.py +60 -0
@@ 1,60 @@
#!/usr/bin/python3

def BinaryTree(r):
    return [r, [], []]

def insertLeft(root,newBranch):
    t = root.pop(1)
    if len(t) > 1:
        root.insert(1,[newBranch,t,[]])
    else:
        root.insert(1,[newBranch, [], []])
    return root

def insertRight(root,newBranch):
    t = root.pop(2)
    if len(t) > 1:
        root.insert(2,[newBranch,[],t])
    else:
        root.insert(2,[newBranch,[],[]])
    return root

def getRootVal(root):
    return root[0]

def setRootVal(root,newVal):
    root[0] = newVal

def getLeftChild(root):
    return root[1]

def getRightChild(root):
    return root[2]

r = BinaryTree(3)
insertLeft(r,4)
insertLeft(r,5)
insertRight(r,6)
insertRight(r,7)
l = getLeftChild(r)
print(l)

setRootVal(l,9)
print(r)
insertLeft(l,11)
print(r)
print(getRightChild(getRightChild(r)))


def build_tree():
    r = BinaryTree("a")
    insertLeft(r,'b')
    left = getLeftChild(r)
    insertRight(left, 'd')
    insertRight(r,'c')
    right = getRightChild(r)
    insertLeft(right,'e')
    insertRight(right,'f')
    return r

tree = build_tree()

A  => chapter7/tree_asrefererence.py +61 -0
@@ 1,61 @@
#!/usr/bin/python3

class BinaryTree:
    def __init__(self,rootObj):
        self.key = rootObj
        self.leftChild = None
        self.rightChild = None

    def insertLeft(self,newNode):
        if self.leftChild == None:
            self.leftChild = BinaryTree(newNode)
        else:
            t = BinaryTree(newNode)
            t.leftChild = self.leftChild
            self.leftChild = t

    def insertRight(self,newNode):
        if self.rightChild == None:
            self.rightChild = BinaryTree(newNode)
        else:
            t = BinaryTree(newNode)
            t.rightChild = self.rightChild
            self.rightChild = t


    def getRightChild(self):
        return self.rightChild

    def getLeftChild(self):
        return self.leftChild

    def setRootVal(self,obj):
        self.key = obj

    def getRootVal(self):
        return self.key


r = BinaryTree('a')
print(r.getRootVal())
print(r.getLeftChild())
r.insertLeft('b')
print(r.getLeftChild())
print(r.getLeftChild().getRootVal())
r.insertRight('c')
print(r.getRightChild())
print(r.getRightChild().getRootVal())
r.getRightChild().setRootVal('hello')
print(r.getRightChild().getRootVal())

def build_tree():
    r = BinaryTree('a')
    r.insertLeft('b')
    r.insertRight('c')
    r.getLeftChild().insertRight('d')
    r.getRightChild().insertLeft('e')
    r.getRightChild().insertRight('f')
    return r

tree = build_tree()
print(tree)

A  => chapter7/tree_traversal.py +84 -0
@@ 1,84 @@
#!/usr/bin/python3

from pythonds.trees import BinaryTree
import operator

def preorder(tree):
    if tree:
        print(tree.getRootVal())
        preorder(tree.getLeftChild())
        preorder(tree.getRightChild())


def preorder(self):
    print(self.key)
    if self.leftChild:
        self.leftChild.preorder()
    if self.rightChild:
        self.rightChild.preorder()


def postorder(tree):
    if tree != None:
        postorder(tree.getLeftChild())
        postorder(tree.getRightChild())
        print(tree.getRootVal())

def postordereval(tree):
    opers = {'+':operator.add, '-':operator.sub, '*':operator.mul, '/':operator.truediv}
    res1 = None
    res2 = None
    if tree:
        res1 = postordereval(tree.getLeftChild())
        res2 = postordereval(tree.getRightChild())
        if res1 and res2:
            return opers[tree.getRootVal()](res1,res2)
        else:
            return tree.getRootVal()


def inorder(tree):
  if tree != None:
      inorder(tree.getLeftChild())
      print(tree.getRootVal())
      inorder(tree.getRightChild())


def printexp(tree):
  sVal = ""
  if tree:
      if tree.getLeftChild() is None:
          sVal = printexp(tree.getLeftChild())
      else:
          sVal = '(' + printexp(tree.getLeftChild())
      sVal = sVal + str(tree.getRootVal())
      if tree.getLeftChild() is None:
          sVal = sVal + printexp(tree.getRightChild())
      else:
          sVal = sVal + printexp(tree.getRightChild())+')'
  return sVal

# Tree traversals examples

r = BinaryTree('a')
r.insertLeft('b')
r.insertRight('c')
r.getRightChild().insertRight('helloRight')
r.getLeftChild().insertLeft('helloLeft')

inorder(r)

# Evaluating expressions

r = BinaryTree('*')
r.insertLeft('+')
r.getLeftChild().insertLeft(10)
r.getLeftChild().insertRight(20)
r.insertRight(2)

# (((10)+(20))*(2)) -> ((10+20)*2)


print(postordereval(r))

print(printexp(r))

A  => chapter8/graph.py +73 -0
@@ 1,73 @@
#!/usr/bin/python3


class Vertex:
    def __init__(self,key):
        self.id = key
        self.connectedTo = {}

    def addNeighbor(self,nbr,weight=0):
        self.connectedTo[nbr] = weight

    def getConnections(self):
        return self.connectedTo.keys()

    def getId(self):
        return self.id

    def getWeight(self,nbr):
        return self.connectedTo[nbr]

    def __str__(self):
        return str(self.id) + ' connectedTo: ' + str([x.id for x in self.connectedTo])


class Graph:
    def __init__(self):
        self.vertList = {}
        self.numVertices = 0

    def addVertex(self,key):
        self.numVertices = self.numVertices + 1
        newVertex = Vertex(key)
        self.vertList[key] = newVertex
        return newVertex

    def addEdge(self,f,t,weight=0):
        if f not in self.vertList:
            nv = self.addVertex(f)
        if t not in self.vertList:
            nv = self.addVertex(t)
        self.vertList[f].addNeighbor(self.vertList[t], weight)

    def getVertex(self,n):
        if n in self.vertList:
            return self.vertList[n]
        else:
            return None

    def getVertices(self):
        return self.vertList.keys()

    def __contains__(self,n):
        return n in self.vertList

    def __iter__(self):
        return iter(self.vertList.values())


g = Graph()
for i in range(6):
   g.addVertex(i)
print(g.vertList)
g.addEdge(0,5,2)
g.addEdge(1,2,4)
g.addEdge(2,3,9)
g.addEdge(3,4,7)
g.addEdge(3,5,3)
g.addEdge(4,0,1)
g.addEdge(5,4,8)
g.addEdge(5,2,1)
for v in g:
   for w in v.getConnections():
       print("( %s , %s )" % (v.getId(), w.getId()))

A  => chapter8/word_ladder.py +56 -0
@@ 1,56 @@
#!/usr/bin/python3

from pythonds.graphs import Graph

def buildGraph(wordFile):
    d = {}
    g = Graph()
    wfile = open(wordFile,'r')
    # create buckets of words that differ by one letter
    for line in wfile:
        word = line[:-1]
        for i in range(len(word)):
            bucket = word[:i] + '_' + word[i+1:]
            if bucket in d:
                d[bucket].append(word)
            else:
                d[bucket] = [word]
    # add vertices and edges for words in the same bucket
    for bucket in d.keys():
        for word1 in d[bucket]:
            for word2 in d[bucket]:
                if word1 != word2:
                    g.addEdge(word1,word2)
    return g



from pythonds.graphs import Graph, Vertex
from pythonds.basic import Queue

def bfs(g,start):
  start.setDistance(0)
  start.setPred(None)
  vertQueue = Queue()
  vertQueue.enqueue(start)
  while (vertQueue.size() > 0):
    currentVert = vertQueue.dequeue()
    for nbr in currentVert.getConnections():
      if (nbr.getColor() == 'white'):
        nbr.setColor('gray')
        nbr.setDistance(currentVert.getDistance() + 1)
        nbr.setPred(currentVert)
        vertQueue.enqueue(nbr)
    currentVert.setColor('black')


def traverse(y):
    x = y
    while (x.getPred()):
        print(x.getId())
        x = x.getPred()
    print(x.getId())


g = buildGraph()
traverse(g.getVertex('sage'))