You are on page 1of 337

Data Structures and Algorithms

Prepared by: Dr. Heba El Hadidi

2022-2023
Data Structures and Algorithms with Python

Contents Page
Chapter one Introduction 3
Chapter Two Complexity Analysis 16
Chapter Three Searching and Sorting 40
Chapter Four Python Programming Introduction 88
Chapter Five Python Collections 104
Chapter Six Stack and Queue 138
Chapter Seven Arrays and Linked lists 186
Chapter Eight Trees 270
Chapter Nine Graphs 286
Chapter Ten Recursion
Chapter Eleven Hashing
Chapter Twelve Strings Algorithms
String Operations
The Boyer-Moore Algorithm
The Knuth-Morris-Pratt Algorithm
Hash-Based Lexicon Matching
Tries
Chapter Thirteen Heaps
Chapter Fourteen Exercises

Second Level Students 2021/2022 1


Data Structures and Algorithms with Python

COURSE OBJECTIVES:
The course should enable the students to:
1- Learn the basic techniques of algorithm analysis.
2- Demonstrate several searching and sorting algorithms.
3- Implement linear and non-linear data structures.
4- Demonstrate various tree and graph traversal algorithms.
5- Analyze and choose appropriate data structure to solve problems
in real world.

1. Introduction

Data Structures Introduction


This course presents the fundamentals of data structures (DS) and
algorithms – the basic elements from which large and complex
software projects are built. To understand data structure, you need
to:
understand information arrangement in the computer memory;
become familiar with the algorithms for manipulating the
information contained in the DS; and understand the performance
characteristics for the DS so that you can select a suitable DS for a
particular application, you are able to make an appropriate decision.

The Role of DS in Computation


Efficient programs rely on DS for problem solving in many
applications.

➢ Makes Computations Faster:


• Faster is better. (Another way to make computations faster is to use
parallel or distributed computation.)

Second Level Students 2021/2022 2


Data Structures and Algorithms with Python

❖ Is any program correct?


❖ Is any correct program efficient?
❖ How can you decide?

➢ You should know that:

Program: Algorithm + DataStructure +Implementation.


• Algorithm
✓ The basic method; it determines the data-items computed.
✓ Also, the order in which those data-items are computed (and
hence the order of read/write data-access operations).
• Data structures
- Supports efficient read/write of data-items used/computed.
Total Time = Time to access/store data + Time to compute data.

Efficient Algorithm = Good method + Good data-structure (+ Good


Implementation)

Some Common Terms:


• A variable refers to a specific location in computer memory
used to store one and only one item.
• The data type of a variable indicates the set in which the
variable takes its values.
• A statement, ‘line of code’, describes a one step to follow in
the algorithm.

Second Level Students 2021/2022 3


Data Structures and Algorithms with Python

Data Types:
Computer memory is filled with zeros and ones. If we have a
problem and we want to code it, it is very difficult to provide the
solution in terms of zeros and ones. To help users, programming
languages and compilers provide us with data types. For example,
integer takes 2 bytes (actual value depends on compiler), float takes
4 bytes, etc. At the top level, there are two types of data types:
➢ System-defined data types (also called Primitive data types)
Data types that are defined by system. The primitive data types
provided by many programming languages are: int, float, char,
double, bool, etc. The number of bits allocated for each primitive
data type depends on the programming languages, the compiler, and
the operating system. For the same primitive data type, different
languages may use different sizes. Depending on the size of the data
types, the total available values (domain) will also change.

➢ User-defined data types (Non-Primitive data types)

Algorithms
➢ What is an Algorithm?
An algorithm is an explicit, precise, unambiguous sequence of
elementary simple instructions, be followed to solve a problem.
Normally, people write algorithms only for difficult tasks. Algorithms
explain how to find the solution to a complicated algebra problem,

Second Level Students 2021/2022 4


Data Structures and Algorithms with Python

how to find the shortest path through a network containing


thousands of streets, or how to find the best mix of hundreds of
investments to optimize profits.
Computer programs are concrete representations of algorithms, but
algorithms are not programs. Rather, algorithms are abstract
mechanical procedures that can be implemented in any
programming language that supports the underlying primitive
operations. The clearest way to present an algorithm is using a
combination of pseudocode and structured English. Pseudocode uses
the structure of formal programming languages and mathematics to
break algorithms into primitive steps.
• You need to understand the following:
The algorithm’s behavior Does it find the best possible solution, or
does it just find a good solution? Could there be multiple best
solutions? Is there a reason to pick one “best” solution over the
others?
The algorithm’s speed Is it fast? Slow? Is it usually fast but
sometimes slow for certain inputs?
The algorithm’s memory requirements How much memory will the
algorithm need? Is this a reasonable amount? Does the algorithm
require billions of terabytes more memory than a computer could
possibly have (at least today)?

Second Level Students 2021/2022 5


Data Structures and Algorithms with Python

The main techniques the algorithm uses Can you reuse those
techniques to solve similar problems?

Note: If the program is run on a large data set, then the running time
becomes an issue.

Algorithms Properties:
1) Precision:
The steps are precisely defined (understand). E.g. (instruction
Add 6 or 7 to x) is not allowed, as it is not clear).
2) Correctness: The output is correct for each input as defined by
the problem.
3) Finiteness: The algorithm produces the output after a finite
number of steps for each input.
4) Determinism
5) Generality:
Applicable for all problems of the desired form, not a specific
case.
The importance of algorithm analysis. you should be able to find
the complexity of any given algorithm.
An algorithm is a step-by-step procedure for performing some task in
a finite amount of time, and a data structure is a systematic way of
organizing and accessing data. These concepts are central to
computing.
A primary analysis tool is to characterize the running time of an
algorithm or data structure operation, with space usage also being of
interest. It is an important consideration in economic and scientific
Second Level Students 2021/2022 6
Data Structures and Algorithms with Python

applications since everyone expects computer applications to run as


fast as possible. The fastest algorithm is the minimum running time
and minimum storage. Running time is one of the major
characteristics that judges the efficiency of algorithms. Running time
may be proportional to one of these functions.
n --- inputs
O(1) and Log(n) the best running time.
n log(n), n2, n3, …O(2n), O(n!) more complex.

Time O(n!) O(2n)


O(n2)
O(n log n)

O(n)

O(log n)

O(1)
Input n
Fig. 1-1
Another factor for algorithm efficiency is storage space of memory. A
double variable takes large space, what if they become 10 double
variables so the storage >>>>.

Variables. What is variable?


Consider the mathematical function: x2- 4y=1 (x and y are
placeholders for data). Similarly, in computer science programming
we need something to hold data, and variables is the way to do
that.

Second Level Students 2021/2022 7


Data Structures and Algorithms with Python

Problem: Compute the average of three numbers.


Two Methods: (1) aver = (x + y + z)/3. two less div-operations than (2)
(2) aver = (x/3) + (y/3) + (z/3).

Data structures:
(a) Three variables x, y, z.
(b) An array nums[0..2].
- accessing an array-item takes more time than accessing a simple
variable. (To access nums[i], the executable code must compute its
address
addr(nums[i]) = addr(nums[0]) + i*sizeof(int), which involves 1
addition and 1 multiplication.)
Example: Selection problem
Given a list of N numbers, determine the kth largest, where k≤ N
Algorithm 1 Algorithm 2
1- Read N numbers into 1- Read the first k elements into an array.
an array. 2- Sort them in descending order
2- Sort the array in 3- Each remaining element is read one by
descending order by one:
simple algorithm -if smaller than the kth element, then it is
3- Return the element in ignored.
position k - otherwise, it is placed in its correct spot in
the array bumping one element out of the
array
4- The element in the kth position is
returned as the answer.

Which algorithm is better when:


- N=100 and k=100
- N=100 and k=1
What happens when N=1,000,000 and k= 500,000?
Are there better algorithms?
Second Level Students 2021/2022 8
Data Structures and Algorithms with Python

Analyzing Algorithms
The efficiency (running time) of an algorithm or data structure,
depends on number of factors. If an algorithm has been
implemented, we can study its running time by executing it on
various test inputs and recording the actual time spent in each
execution. Such measurements can be taken in an accurate manner
by using system calls that are built into the language or operating
system for which the algorithm is written. (Experiments)
In general, the running time of an algorithm or data structure method
increases with the input size, although it may also vary for distinct
inputs of the same size.
Also, the running time is affected by the hardware environment
(processor, clock rate, memory, disk, etc.) and software
environment (operating system, programming language, compiler,
interpreter, etc.) in which the algorithm is implemented, compiled,
and executed. All other factors being equal, the running time of the
same algorithm on the same input data will be smaller if the
computer has, say, a much faster processor or if the implementation
is done in a program compiled into native machine code instead of
an interpreted implementation run on a virtual machine.
- We only analyse correct algorithms
- An algorithm is correct iff ∀input instance ∃ correct output

Second Level Students 2021/2022 9


Data Structures and Algorithms with Python

- Analysig an algorithm= predicting the resources that the


algorithm requires
- Resources include:
Memory, Communiction bandwidth, Computational time (usually
most important)
- The factors affecting the running time:
• Computer
• Compiler
• Algorithm used
• Input to the algorithm:
❖ The content of the input affects the running time
❖ Typically, the input size (no. of items in the input) is the
main consideration
❖ E.g., sorting algorithm- the number of items to be sorted
❖ E.g. multiply two matrices- the total number of elements
in the two matrices
- Machine model assumed:
Instructions are executed one after another, with no concurrent
operations (Not parallel computers)
• Algorithms with a constant, logarithmic or linear complexity are
so fast that we cannot feel any delay, even with a relatively big
size of the input data.

Second Level Students 2021/2022 10


Data Structures and Algorithms with Python

• Complexity O(n*log(n)) is similar to the linear and works nearly


as fast as linear, so it will be very difficult to feel any delay.
• Quadratic algorithms work very well up to several thousand
elements.

• Cubic algorithms work well if the elements are not more than
1,000.

• Generally, these so-called polynomial algorithms (any, which are


not exponential) are fast and working well for thousands of
elements.

• Note, the exponential algorithms do not work well and we


should avoid them.

Program 1 Program 2 Program 3


x=x+2; for k=1 to n{ for i=1 to n{
x=x+2;} for x=1 to n{
x=x+2;
} }
1 n+1 for n+1 for i
n assign n(n+1) for j
=2n+1 n2 assign
=2n2 +2n+ 1
O(1) O(n) O(n2)

Second Level Students 2021/2022 11


Data Structures and Algorithms with Python

Pseudo-Code ‫غير معتمد علي لغة معينة‬


Programmers are often asked to describe algorithms in a way that is
intended for human eyes only. Such descriptions are not computer
programs but are more structured than usual prose. They also
facilitate the high-level analysis of a data structure or algorithm. We
call these descriptions pseudocode.
Write the lines of codes independent of all language specific words,
terms or notation.
- Use bold to denote special terms/actions
- Restrict “=” to testing equality, and “:=” for assigning values
X:=e (assignment statement)
➔ Evaluate the expression e, assign the result to x.
The left-hand side can only be one variable.
The right-hand side can be any viable expression.
x:=1 assign 1 to x.
x:=x+1 x value is 2.

Example. Compute the number of positive and negative items in


nums[0. . n - 1]; assume each nums[i] ≠0.

Pseudocode:
1. Initialize positiveCount = 0.
2. Use each nums[i] > 0 to increment positiveCount by one.
3. Let negativeCount = n − positiveCount.

Second Level Students 2021/2022 12


Data Structures and Algorithms with Python

Code:
positiveCount = negativeCount = 0;
for (i=0; i<n; i++) //each nums[i] ¹ 0
if (0 < nums[i]) positiveCount++;
else negativeCount++;

An Example of Pseudo-Code
The array-maximum problem is a simple problem of finding the
maximum element in an array A storing n integers. To solve this
problem, we can use an algorithm called arrayMax, which scans
through the elements of A using a for loop.

The pseudocode description of algorithm arrayMax is:


Algorithm arrayMax(A, n):
Input: An array A storing n ≥ 1 integers.
Output: The maximum element in A.
currentMax ← A[0]
for i ← 1 to n − 1 do
if currentMax < A[i] then
currentMax ← A[i]
return currentMax
Algorithm 1.2: Algorithm arrayMax.

Programming
Q: Is Programming and programming language the same thing?
Ans: No.
Programming language is a tool we use to program – Ex: Photoshop
is not the image.

Second Level Students 2021/2022 13


Data Structures and Algorithms with Python

You learn programming for years. Is it so difficult? No- you just need
some time to become efficient programmer. During learning
programming, you need to learn algorithms (Mohamed Ibn Mosa Al
Khawarezmy).
to solve a particular problem‫الخوارزميات ببساطة هي خطوات لحل منطقي‬
‫ خوارزمية لطباعة مستند وورد‬:‫مثال بسيط علي خوارزمية حياتية‬
<=== ‫أنا كمبرمج ممكن أكتب نفس البرنامج بطرق مختلفة لكن كيف أقيم أفضلها؟‬
‫ ليس من المهم ان تقول انا اتعلمت بايثون انما االهم هل‬.‫ هكذا تكون مبرمج‬algorithm
‫تعرف تجيد استخدام أدواتك وتفكر أفضل؟‬

Programming teaches you how to think.

Algorithm refers to a step by step process for performing some


action.

.‫تذكر الكمبيوتر يتبع تعليماتك‬


Interpreter & Compiler

Computer Programming language Human

Understand Interpreter Speak any


0 and 1 only & compiler language

Fig. 1-2

‫المترجم وسيط فما هو المترجم؟؟؟‬

This section presents a quick overview of Python programming.

Second Level Students 2021/2022 14


Data Structures and Algorithms with Python

2. Complexity Analysis
After completing this chapter, you will be able to:
✓ Analysis of the algorithm’s computational complexity.

If there are more than one way to solve a problem. We need to


compare the performance of different algorithms and choose the
best one to solve this problem. While analyzing an algorithm, we
mostly consider time complexity and space complexity. Time
complexity of an algorithm quantifies the amount of time taken by an
algorithm to run as a function of the length of the input. Similarly,
Space complexity of an algorithm quantifies the amount of space or
memory taken by an algorithm to run as a function of the length of
the input.

Time and space complexity depend on lots of things like hardware,


operating system, processors, etc. However, we do not consider any
of these factors while analyzing the algorithm. We will only consider
the execution time of an algorithm.

‫؟‬time, space ‫ من‬your algorithm ‫كم احتاج‬


For example, to search for the number 10:

0 1 2 3 4 5
10 5 15 2 25 55
Best case average case worst case

Second Level Students 2021/2022 15


Data Structures and Algorithms with Python

Time complexity is 3 cases :‫عند البحث عن عنصر في مصفوفة قد‬


best case ‫ تجده في أول مكان بالمصفوفة هنا‬-1
)55 ‫ (مثال لو نبحث عن‬worst case ‫ أو في آخر مكان بالمصفوفة هنا‬-2
)2 ‫ (مثال لو نبحث عن‬average/middle case ‫ أو في النصف‬-3
Some references call these cases lower bound- middle bound- upper
bound
best case is referred as → Omega notation Ω
average case is referred as → theta notation ϴ
worst case is referred as → Big O notation (O order)
‫ نهتم‬ϴ ‫ أو‬Ω ‫ومنطقيا نركز علي أسوأ الحاالت لمحاولة عالجها لألفضل فلن نهتم ب‬
‫؟‬best/average/worst case ‫ لنعرف في الكود احنا في‬Big O ‫فقط ب‬
• Worst-case running time of an algorithm
- The longest running time for any input of size n.
- The upper bound on the running time for any input (guarantee
that the algorithm will never take longer).
- Example: sort a set of numbers in increasing order; and the
data is in descending order.
- The worst-case can occur often.
• Best-case running time
Sort a set of numbers in increasing order; and the data is
already in increasing order.
• Average-case running time
May be difficult to define what “average” means.

Experimental studies on running times are useful, but they have


some limitations:
• Experiments can be done only on a limited set of test inputs.
• It is difficult to compare the efficiency of two algorithms unless
experiments on their running times have been performed in the same
hardware and software environments.

Second Level Students 2021/2022 16


Data Structures and Algorithms with Python

• It is necessary to implement and execute an algorithm in order to


study its running time experimentally.
Thus, while experimentation has an important role to play in
algorithm analysis, it alone is not sufficient. Therefore, in addition to
experimentation, we desire an analytic framework that takes into
account all possible inputs
• Allows us to evaluate the relative efficiency of any two algorithms in
a way that is independent from the hardware and software
environment

This methodology aims at associating with each algorithm a function


f(n) that characterizes the running time of the algorithm in terms of
the input size n. Typical functions that will be encountered include n
and n2.
For example, we will write statements of the type “Algorithm A runs in
time proportional to n,” meaning that if we were to perform
experiments, we would find that the actual running time of algorithm
A on any input of size n never exceeds cn, where c is a constant that
depends on the hardware and software environment used in the
experiment. Given two algorithms A and B, where A runs in time
proportional to n and B runs in time proportional to n2, we will prefer
A to B, since the function n grows at a smaller rate than the function
n2.

Measuring the Run Time of an Algorithm


One way to measure the time cost of an algorithm is to use the
computer’s clock to obtain an actual run time. This process, called
benchmarking or profiling, starts by determining the time for
several different data sets of the same size and then calculates the
average.

Second Level Students 2021/2022 17


Data Structures and Algorithms with Python

# H:\data structures\data structures Python 2020\summer2021\codes\lec1-1.py


# Prints the running times for problem sizes

import time
problemSize = 10000000
print("%12s%16s" % ("Problem Size", "Seconds"))
for count in range(5):
start = time.time()
# The start of the algorithm
work = 1
for x in range(problemSize):
work += 1
work -= 1
# The end of the algorithm
elapsed = time.time() - start
print("%12d%16.3f" % (problemSize, elapsed))
problemSize *= 2

out:
Problem Size Seconds
10000000 1.234
The program uses the time() function in the time module to track the
running time. This function returns the number of seconds that have
elapsed between the current time on the computer’s clock and
January 1, 1970 (also called The Epoch). Thus, the difference
between the results of two calls of time.time() represents the elapsed
time in seconds.

However, there are two major problems with this technique:


➢ Different hardware platforms have different processing speeds,
so the running times of an algorithm differ from machine to
machine. Also, the running time of a program varies with the
type of operating system that lies between it and the hardware.
Finally, different programming languages and compilers produce
code whose performance varies. For example, the machine code of

Second Level Students 2021/2022 18


Data Structures and Algorithms with Python

an algorithm coded in C usually runs slightly faster than the byte


code of the same algorithm in Python.
Thus, predictions of performance generated from the results of
timing on one hardware or software platform generally cannot be
used to predict potential performance on other platforms.
➢ It is impractical to determine the running time for some
algorithms with very large data sets. For some algorithms, it
doesn’t matter how fast the compiled code or the hardware
processor is. They are impractical to run with very large data
sets on any computer.

Counting Instructions
Another technique used to estimate the efficiency of an algorithm is
to count the instructions executed with different problem sizes.
These counts provide a good predictor of the amount of abstract
work an algorithm performs, no matter what platform the algorithm
runs on. Keep in mind, however, that when you count instructions,
you are counting the instructions in the high-level code in which the
algorithm is written, not instructions in the executable machine
language program.
When analyzing an algorithm in this way, you distinguish between
two classes of instructions:
➢ Instructions that execute the same number of times regardless
of the problem size
➢ Instructions whose execution count varies with the problem
size.
For now, you ignore instructions in the first class, because they do
not figure significantly in this kind of analysis. The instructions in the
second class normally are found in loops or recursive functions. In

Second Level Students 2021/2022 19


Data Structures and Algorithms with Python

the case of loops, you also zero in on instructions performed in any


nested loops or, more simply, just the number of iterations that a
nested loop performs.

# H:\data structures\data structures Python 2020\summer2021\codes\lec1-2.py


# Prints the number of iterations for problem sizes
# that double, using a nested loop.

problemSize = 1000
print("%12s%15s" % ("Problem Size", "Iterations"))
for count in range(5):
number = 0
# The start of the algorithm
work = 1
for j in range(problemSize):
for k in range(problemSize):
number += 1
work += 1
work -= 1
# The end of the algorithm
print("%12d%15d" % (problemSize, number))
problemSize *= 2

As you can see, the number of iterations is the square of the problem
size (Fig. 2-1).

Fig. 2-1 The program output counts iterations

Second Level Students 2021/2022 20


Data Structures and Algorithms with Python

Questions:
Write a program that counts and displays the number of
iterations of the Following loop:
while problemSize > 0:
problemSize = problemSize // 2

The Big-Oh Notation

Let f(n) and g(n) be functions mapping positive integers to positive


real numbers. We say that f(n) is O(g(n)) if there is a real constant c>0
and an integer constant no ≥1 such that f(n) ≤ c g(n), for n≥ no.

This definition is often referred to as the “big-oh” notation,


“pronounced as f(n) is big-oh of g(n)”.

‫هي طريقة لتقييم الخوارزميات وتعتمد علي حساب عدد خطوات البرنامج أو خطوات التنفيذ‬
‫ بدل ما ننفذ الكود علي أكثر من جهاز كمبيوتر وتقييمه فممكن‬step execution
‫ في التقييم أفضل‬big O ‫مواصفات األجهزة تتحكم في التقييم فيستخدم‬
‫ في سطور في البرنامج تدخل في الحساب وسطور ال تدخل‬big O ‫عند حساب‬
1 ‫ هذه عبارة تخصيص تحسب‬int c=10 ‫ ال تحسب بينما‬int c ‫مثال‬
public void max(int a, int b) no
{ no
…program instructions
} no

Examples:
(1) 1+4N= O(N)

(2) 7N2+10N+3= O(N2)

Second Level Students 2021/2022 21


Data Structures and Algorithms with Python

(3) 20n3 +10 n log n+ 5 is O(n3). As 20 n3 +10 n log n+ 5 ≤ 35 n3, for n


≥1

(4) 3 log n+ 2 is O(log n). 3 logn+2 ≤ 5logn, for n ≥ 2. Note that log n
is zero for n = 1

(5) log10 N=log2 N/log2 10= O(log2 N)=O(log N)

(6) 2n+2 is O(2n). As 2n+2 = 2n ·22 = 4·2n; hence, we can take c = 4 and n0
= 1 in this case.
(7) 2n+100 log n is O(n). As 2n+100 log n ≤ 102 n, for n ≥ n0 = 1;
hence, we can take c= 102 in this case.
(8) sin N=O(1), 10=O(1), 1010=O(1)
(9) ∑𝑁 2
𝑖=1 𝑖 ≤ 𝑁. 𝑁 = 𝑂(𝑁 )

(10) log N + N=O(N)

• A basic operation is an operation which takes a constant


amount of time to execute.
• Example of Basic Operations:

Arithmetic operations: *, /, %, +, -

Boolean operations: AND, OR, NOT

Assignment statements of simple data types.

Reading / writing of primitive types

Simple conditional tests: if (x < 12) ...

method calls (Note: the execution time of a method itself may


not be constant)

a method's return statement.

Second Level Students 2021/2022 22


Data Structures and Algorithms with Python

Memory Access (includes array indexing)

We consider an operation such as ++, +=, and *= as


consisting of two basic operations.

• Note: To simplify complexity analysis we shall not consider


memory access (fetch or store) operations

• A linear loop is one in which the loop index is updated by


either addition or subtraction. for(int i = k; i < n; i++)

• A loop is independent if its index values are independent


of an outer loop index.

• Two nested loops are dependent if the inner index is


dependent of the outer index.

SE (step execution)

Example 2-1:
Write method to calculate total= 1+2+ 3+ ….+ n
Number of iterations
public int sum (int n) 0
{ 0
int i, total; 0
total =0; 1
for (i=1; i<=n; i++) n+1
{ 0
total= total+i; 2*n # 2 because assign, sum
} 0
return total; 1
Second Level Students 2021/2022 23
Data Structures and Algorithms with Python

}
‫ مرات‬7 ‫ مرات ومقارنتها‬6 ‫ تنفذ‬for ‫ اذن‬n=6 ‫مثال‬
So SE= 1+ n+1 + 2n +1= 3n+3 ➔ O(n) do not consider
constants
‫ سيزداد وقت تنفيذ الكود‬n ‫اذن في هذا الكود كلما زادت‬

Example 2-2:
𝑛(𝑛+1)
Write method to calculate ∑𝑛𝑖=1 𝑖 =
2
public int sum (int n) 0
{ 0
int total; 0
total= n*(n+1)/2; 1+1+1+1 #assign,mul,add,div
return total; 1
}
SE =4+1=5 =O(5)➔ O(1) ‫عدد الجمل‬
Note: O(1000000) = O(1) ‫ال تعني جملة واحدة‬
)n ‫لكنها تعني أن الكود ليس دالة زمنية مهما نفذته يأخذ نفس الوقت (لن يعتمد علي‬
Example 2-3:
Write method to calculate ∑20
𝑖=1 𝑖 i.e. sum= 1+2+3+ ….+20
public int sum ( ) 0
{ 0
int i, total; 0
total=0; 1
for (i=1; i<=20; i++) 20+1
{ 0
total=total+i; 2*20 #assign, add 20 time
} 0

Second Level Students 2021/2022 24


Data Structures and Algorithms with Python

return total; 1
} 0
SE= 1+21+40+1=63
Step execution O(63)= O(1) or O(c)
Example 2-4:
int sumList(int A[ ], int n) 0
{ 0
int sum=0, i; 1
for(i=0; i< n; i++) n+1
sum= sum+A[i]; 3n # array access, sum, assign
return sum; 1
}
Total= 1+ n+1 +3n +1= 4n+3 O(n)

Example 2-5:
Compute SE (step execution) and big O for the following code:
x=0; 1
for (i= -2; i<n; i++) 2+n+1
{ 0
x=x+i; 2*(n+2)
y=x+2; 2*(n+2)
} 0
print(x); 1
SE=1+n+3+2n+4+2n+4+1=5n+13 ➔ O(n)

Example 2-6:
Compute SE (step execution) and big Oh for the following code:
float sum (float list[ ], int n) 0

Second Level Students 2021/2022 25


Data Structures and Algorithms with Python

{ float total=0; 1
int i; 0
for (i=0; i<n; i++) n+1
total+=list[i]; 3n # array access, sum
return total; 1
} SE=1+n+1+3n+1=4n+3 ➔ Time Complexity: O(n)

Example 2-7:
Compute SE (step execution) and big O for the following code:
int i; 0
i=1; 1
for(i; i<n; i= i*2) log (n) *2 so the log base is 2
print(i); 1
SE=1+ log2(n) + 1 ➔ O(log2 (n))

Example 2-8:
Write a method to calculate ∑𝑁 3
𝑖=1 𝑖 . Compute SE (step execution) and

big O.
int sum (int n)
{
int total; 0
total=0; 1 (one operation)
for (int i=1; i<=n; i++) n+1
total+=i*i*i; 4n (4 operations: assign, add, 2mul, n times)
return total; 1
}
SE=1+ n+1+4n+1 ➔ O(n)

Second Level Students 2021/2022 26


Data Structures and Algorithms with Python

Example 2-9:
Add two matrices C(m, n)= A(m, n)+ B(m, n)
Algorithm add(A, B, m, n)
Input: +ve integers m, n and two dimensional arrays of numbers A and
B (rows 1:m, cols 1:n)
Output: a two-dimensional array C, the sum of A and B

for i← 0 to m-1 m+1


for j← 0 to n-1 n+1
C[i, j] ←A[i, j]+B[i,j] m*n*5 (=, +, 3access)

end for
end for
return C mn (access matrix C)
SE= m+1+n+1+6mn O(n2)
Example 2-10:
work = 1 1
for x in range(problemSize): n
work += 1 2*(n-1)
work -= 1 1
SE= 1+n+2n-2+1 O(n)

Example 2-11 while:


k= 1
while k<=n:
k=k*2
O(log n)

Second Level Students 2021/2022 27


Data Structures and Algorithms with Python

Example 2-12:
What is the time Complexity of the following code:
I=n
while I>0:
for j in range(n): n
print(j)
I=I/2 log n inside while
 O(n log n)
Example 2-13: (nested for)
Compute SE (step execution) and big O for the following code:
int func(a[ ], n) 0
{ 0
int x=5; 1
for (i=1; i<=n; i++) n+1
{ 0
for (j=1; j<n; j++) n
{ 0
x=x+ i + j; n-1 *n ‫تجاهلنا تكرار الجمع‬
print(x); n-1 ‫عدد مرات تنفيذ‬for
} 0
} 0
} 0
SE= 1+ n+1+ n*[ n+ n-1+ n-1]= n+2+3n2-2n= 3n2-n+2
O(n2)

Second Level Students 2021/2022 28


Data Structures and Algorithms with Python

Example 2-14:
Estimate the time complexity (big O) for the following codes:
def func1(n):
for var1 in range(m):
if m%2 == 0:
for var2 in range(m):
print(m+var1+var2)
return(0)
nested for ➔ O(m2) or O(n2)
Example 2-15:
def func2(n):
for var1 in range(n):
print(var1)
print(‘Hello’)
return(0)
O(n)
Example 2-16:
def func3(n):
for j in range(n):
for k in range(n):
for x in range(n):
print(j,k,x)
return(0)
nested for ➔ O(n3)

Second Level Students 2021/2022 29


Data Structures and Algorithms with Python

Example 2-17:
for (int i=0; i<n;i++){
for(int j=1; j<=i; j*=2){
print(I, j) } O(n log n)

for(int j=1; j<=n; j*=2){


print( j) O( log2 n)
}
Example 2-18:
for y in range(len(list)): O(n)
for k in range(10): # this loop will repeat exactly 10 times so O(1)
print(y+k)
 O(n*1)=O(n)
---------------------------------------------------------------
Example 2-19:
for y in range(len(list)//2): O(n/2) = O(n)

Note:
L is a list so: O(1) ==O(c)
L[1] O(1)
L[i]=0 O(1)
len(L) O(1)
l.append(6) O(1)

Second Level Students 2021/2022 30


Data Structures and Algorithms with Python

Estimate the complexity of the following algorithms, and decide


whether the code written by you will work fast:
Q1)
def constant_algo(items):
result = items[0] * items[0]
print ()
constant_algo([4, 5, 6, 8])
O(c)
Q2)
def linear_algo(items):
for item in items:
print(item)
for item in items:
print(item)
linear_algo([4, 5, 6, 8])
O(2n) >>> O(n)

Q3)
def linear_algo(items):
for item in items: execute 4 times
print(item)
linear_algo([4, 5, 6, 8]) O(n)
a single loop from 1 to N, its complexity is linear – O(N)

Q4)
def quadratic_algo(items):
for item in items:
for item2 in items:

Second Level Students 2021/2022 31


Data Structures and Algorithms with Python

print(item, ' ' ,item)


quadratic_algo([4, 5, 6, 8]) O(n*n)
two of nested loops from 1 to N, their complexity is quadratic – O(N2).

Q5)
for i in range(5):
print ("Python is awesome")
O(5)

for item in items:


print(item) O(n)

Q6)
def fact(n):
product = 1
for i in range(n):
product = product * (i+1)
return product
print (fact(5))
Q7)
def fact2(n):
if n == 0:
return 1
else:
return n * fact2(n-1)
print (fact2(5))

Second Level Students 2021/2022 32


Data Structures and Algorithms with Python

Q8)
for (int i = 1; i <= N; i++) { N2
for (int j = 1; j <= N; j++) {
statement1;
}
}
for (int i = 1; i <= N; i++) {
statement2; 4N
statement3;
statement4;
statement5;
}
N2+4N O(N2)
How many statements will execute if N = 10? If N = 1000?

Example: (nested for)


Compute SE (step execution) and big O for the following code:
int i, j; 0
i=j=0; 2
for(i; i<n; i++) n+1
for(j; j<n; j=j/3) log n ‫ألنها قسمة‬
print i+j; 1 or 2
SE= 2+ n* log3 n
O(n log3 n)

Second Level Students 2021/2022 33


Data Structures and Algorithms with Python

Example: (nested for) Dependent loops


Compute SE (step execution) and big O for the following code:
int x=1; 1
for (i=1; i<=n; i++) n+1
{ 0
sum=1; n
𝑛(𝑛+1)
for (j=1; j<=i; j++) +1
2

{ 0 because j depends on i
𝑛(𝑛+1)
x= i+j; *2 ‫في هذه الحالة مجموع متسلسلة حسابية‬
2
𝑛(𝑛+1)
y= x+3; *2
2

} 0
} 0
SE= ???
𝑛(𝑛+1)
= 1+n+1+n+ + 1+n(n+1) + n(n+1)
2

O(n2)

Example:
Compute SE (step execution) and big O for the following code:
for (int i=0; i<n; i++) n
for (int j=0; j< n; j++) n
for (int k=0; k<n; k=k*2) log2 n
print i+j+k;
SE=n*n*log n
O(n2 log2 n)

Second Level Students 2021/2022 34


Data Structures and Algorithms with Python

Example:
Compute SE (step execution) and big O for the following code:
for (int i=n/2; i<n; i++) n/2
for (int k=0; k< n; k=k*2) log2 n
for (int j=0; j<n; j=j*2) log2 n
print i+j+k;
SE=(n/2)*log n*log n
O(n log2 n2)
Example:
long Fatorial(int n)
{
if(n==0)
{
return 1;
}
else
{
return n*Factorial(n-1);
}
}
O(N)
Example:
long Fibonacci(int n)
{
if(n==0)
{
return 1;
}
else if (n==1)
{
return 1;

Second Level Students 2021/2022 35


Data Structures and Algorithms with Python

}
else
{

return Fibonacci( n-1)+ Fibonacci( n-2)


}
}
O(2n)
Example:
For calculating the nth number of Fibonacci can be with a linear
complexity as:
long Fibonacci( int n)
{
long f1=1;
long f2=1;
long f3=1;
for (int i=2; i<n; i++)
{
f1=f2+f3;
f3=f2;
f2= f1;
}
return f1;
}

Example: (while)
Compute SE (step execution) and big O for the following code:
int i=0;
while(i<5) { //runs 5 times
i++;
}
O(11) O(c)

Second Level Students 2021/2022 36


Data Structures and Algorithms with Python

Example:
int i = 1;
do
{
i++;
} while(i<=n);
O(n)
Example:
def fib(n, counter):
“”” Count the number of iterations in the Fibonacci function
“””
Sum =1
First=1
Second=1
Count=3
while Count <= n:
counter.increment()
Sum= First+ Second
First= Second
Second= Sum
Count +=1
return sum

Problem Size Iterations


2 0
4 2
8 6
16 14
32 30

Second Level Students 2021/2022 37


Data Structures and Algorithms with Python

Example: (if/Else)
Compute SE and big O for the following code:
if (condition) {
sequence of statements 1
}
else {
sequence of statements 2
}

Here, either sequence 1 will execute, or sequence 2 will execute.


Therefore, the worst-case time is the slowest of the two possibilities:
max(time(sequence 1), time(sequence 2)). For example, if
sequence 1 is O(N) and sequence 2 is O(1) the worst-case time for the
whole if-then-else statement would be O(N).

‫اذن هدفك كمبرمج أن تحاول تحسين الكود من‬


O(n2) > O(n)> O(log n)> O(1)

Efficiency of an algorithm is the number of basic operations it


performs. Big-oh f(N)=O(g(N)). E.g. f(N)=O(N2)

Exercises
1- big-O notation to classify:
(a) 2n + 4n2 + n+1
(b) 3 n2 + 6
(c) n3 + n2 - n
2- For problem size n, algorithms A and B perform n2 and ½ n2 +1/2 n

instructions, respectively. Which algorithm does more work? Are there


particular problem sizes for which one algorithm performs significantly
better than the other? Are there particular problem sizes for which both
algorithms perform approximately the same amount of work?

Second Level Students 2021/2022 38


Data Structures and Algorithms with Python

3. Searching and Sorting


After completing this chapter, you will be able to:

✓ Describe Searching and sorting algorithms.


✓ Describe how the sequential search and binary search algorithms
work.
✓ Describe how the selection sort and quicksort algorithms work.
✓ Analysis of the search and sort algorithm’s computational complexity.

In Computer Science, Searching is the algorithmic process of looking


for a particular value in a collection of items. The value may be a
keyword in a file, a record in a database, a node in a tree or a value in
a list etc.
Lots of information is stored in computer memory, to retrieve
information you need efficient search algorithm.
Example:
➢ Search for the Minimum
Python’s min function returns the minimum or smallest item in a list.

➢ To find the smallest item in an iterable, we use this syntax:

min(iterable, *iterables, key, default)

min() Parameters

• iterable - an iterable such as list, tuple, set, dictionary, etc.


• *iterables (optional) - any number of iterables; can be more
than one

Second Level Students 2021/2022 39


Data Structures and Algorithms with Python

• key (optional) - key function where the iterables are passed


and comparison is performed based on its return value
• default (optional) - default value if the given iterable is empty
Example: Get the smallest item in a list

number = [3, 2, 8, 5, 10, 6]


smallest_number = min(number);

print("The smallest number is:", smallest_number)

Output
The smallest number is: 2

Example : The smallest string in a list

languages = ["Python", "C Programming", "Java", "JavaScript"]


smallest_string = min(languages);

print("The smallest string is:", smallest_string)

Output

The smallest string is: C Programming

Question:
Write code to return with the index of the minimum item of a list, if
the list is not empty and the items are in arbitrary order. Here is the
code for the algorithm, in function indexOfMin:

def indexOfMin(lyst):
"""Returns the index of the minimum item."""
minIndex = 0
currentIndex = 1

Second Level Students 2021/2022 40


Data Structures and Algorithms with Python

while currentIndex < len(lyst):


if lyst[currentIndex] < lyst[minIndex]:
minIndex = currentIndex
currentIndex += 1
return minIndex

Thus, the algorithm must make n - 1 comparisons for a list of size n.


Therefore, the algorithm’s complexity is O(n).
Pseudo code- Search for the Minimum:
min=A[0]
for i=0 to n
if A[i]<min
min=A[i]
end if
End for

There are many searching algorithms:


✓ Linear Search
✓ Binary Search
✓ Jump Search
✓ Interpolation Search
✓ Exponential Search
✓ Ternary Search

Sequential Search (linear search) of a List


Consider an unsorted single dimensional array of integers and we
need to check whether 31 is present in the array or not, search begins
with the first element. Scan the full list until we find the desired
value.
As the first element does not contain the desired value, then the next
element is compared to value 31 and this process continues until the
desired element is found in the sixth position.
Similarly, if we want to search for 8 in the same array, then the search

Second Level Students 2021/2022 41


Data Structures and Algorithms with Python

begins in the same manner, starting with the first element until the
desired element is found. In linear search, we cannot determine that
a given search value is present in the sequence or not until the entire
list is traversed.

def linear_search(obj, item, start=0):


for i in range(start, len(obj)):
if obj[i] == item:
return i
return -1
arr=[1,2,3,4,5,6,7,8]
x=4
result=linear_search(arr,x)
if result==-1:
print ("element does not exist")
else:
print ("element exist in position %d" %result)

A sequential search function that returns the index of a target if it’s


found.
if key in theArray:
print(“The key is in the array”)
else:
print(“The key is not in the array”)
Second Level Students 2021/2022 42
Data Structures and Algorithms with Python

So, The Python code for a sequential search function:

def sequentialSearch(target, lyst):


"""Returns the position of the target item if found, or -1 otherwise."""
position = 0
while position < len(lyst):
if target == lyst[position]:
return position
position += 1
return -1

Best-Case, Worst-Case, and Average-Case Performance for


linear search
An analysis of a sequential search considers three cases:
1. In the worst case, the target item is at the end of the list or not in
the list at all. Then the algorithm must visit every item and perform n
iterations for a list of size n. Thus, the worst-case complexity of a
sequential search is O(n).
2- In the best case, the algorithm finds the target at the first position,
after making one iteration, for an O(1) complexity.
3. The average case, you add the number of iterations required to
find the target at each possible position and divide the sum by n.
Thus, the algorithm performs (n+ n-1 + n-2 + …. + 1)/n or (n+1)/2
iterations. For very large n, the constant factor of 2 is insignificant, so
the average complexity is still O(n).

Second Level Students 2021/2022 43


Data Structures and Algorithms with Python

Clearly, the best-case performance of a sequential search is rare


compared to the average and worst-case performances, which are
essentially the same.
Case Best case Worst case Average case
If item is present 1 n n/2
If item is not present n n n

Binary Search of a Sorted List


A sequential search is necessary for data that are not arranged in any
order. When searching sorted data, you can use a binary search.
For example, consider you look up a person’s number in a phone
book (the hard-copy).
The data in a phone book are already sorted, so you can’t do a
sequential search. Instead, you estimate the name’s alphabetical
position in the book and open the book as close to that position as
possible. After you open the book, you determine if the target name
lies, alphabetically, on an earlier page or a later page, and flip back or
forward through the pages as necessary. You repeat this process until
you find the name or conclude that it’s not in the book.
Now consider an example of a binary search in Python. To begin,
assume that the items in the list are sorted in ascending order (as
they are in a phone book). The search algorithm goes directly to the
middle position in the list and compares the item at that position to
the target.
➢ If there is a match, the algorithm returns the position.
➢ Otherwise, if the target is less than the current item, the
algorithm searches the portion of the list before the middle
position.
➢ If the target is greater than the current item, the algorithm
searches the portion of the list after the middle position.

Second Level Students 2021/2022 44


Data Structures and Algorithms with Python

The search process stops when the target is found, or the current
beginning position is greater than the current ending position.

# Binary Search Algorithm:


▪ Step1 Compare x with the middle position element.
▪ Step 2 If match, return the middle index
▪ Step 3 Else is x>middle element , search in the right half.
▪ Step 4 Else, search in the left half

Example:
Suppose we want to search 10 in a sorted array of elements, then we
first determine the middle element of the array. As the middle item
contains 18, which is greater than the target value 10, so can discard
the second half of the list and repeat the process to first half of the
array. This process is repeated until the desired target item is located
in the list. If the item is found then it returns True, otherwise False.
L=0

R=10

Mid=int(L+(R-L)/2))

=int(0+(10-0)/2))=5

Arr[5]>10 >>go left

L=0, r= mid-1=4

Mid=int(L+(r-L)/2))=2

Arr[2]<10 go right

L=mid+1=3

L=3 ,r=4

Fig. 3-1 Searching for 10 in a sorted array using Binary Search

Second Level Students 2021/2022 45


Data Structures and Algorithms with Python

Here is the code for the binary search function:

Once again, the worst case occurs when the target is not in the list.
How many times does the loop run in the worst case? This is equal to
the number of times the size of the list can be divided by 2 until the
quotient is 1.

For a list of size n, you essentially perform the reduction n/2/2 … /2


until the result is 1. Let k be the number of times you divide n by 2. To
solve for k, you have n/2k=1, and n=2k, and k=log2n.

Thus, the worst-case complexity of binary search is O(log2n).

Second Level Students 2021/2022 46


Data Structures and Algorithms with Python

Case Best case Worst case Average case


If item is present 1 O(log n) O(log n)
If item is not present O(log n) O(log n) O(log n)

Fig. 3-2 The items of a list using binary search for 10

The binary search for the target item 10 requires four comparisons,
whereas a sequential search would have required 10 comparisons.
 This algorithm actually appears to perform better as the problem
size gets larger. Our list of nine items requires at most four
comparisons, whereas a list of 1,000,000 items requires at most only
20 comparisons!
Binary search is certainly more efficient than sequential search.
However, the kind of search algorithm you choose depends on the
organization of the data in the list. There is an additional overall cost
to a binary search, having to do with keeping the list in sorted order.
In a moment, you’ll examine some strategies for sorting a list and
analyze their complexity. But first read a few words about comparing
data items.

Second Level Students 2021/2022 47


Data Structures and Algorithms with Python

Exercise
Suppose that a list contains the values 20, 44, 48, 55, 62, 66, 74, 88,
93, 99 at index positions 0 through 9. Trace the values of the variables
left, right, and midpoint in a binary search of this list for the target
value 90. Repeat for the target value 44.

Print Duplicates in List


Given a list of n numbers, print the duplicate elements in the list.

def printRepeating(self, arr):


size = len(arr)
i=0
print " Repeating elements :: ",
while i < size:
j=i+1
while j < size:

if arr[i] == arr[j]:
print arr[i],
j += 1
i += 1

O(n2)

Exercises:
1- Given a list of n numbers, find the element, which appears
maximum number of times.
2- Given a list of n elements. Find the majority element, which
appears more than n/2 times. Return 0 in case there is no majority
element.
3- Given two list X and Y. Find a pair of elements (xi, yi) such that
xi∈X and yi∈Y where xi+yi=value.
4- Given a sorted list arr[] find the number of occurrences of a
number.
5- Given a list of n elements, write an algorithm to find three
elements in a list whose sum is a given value.
Second Level Students 2021/2022 48
Data Structures and Algorithms with Python

6- Given a sorted list, find a given number. If found return the index if
not, find the index of that number if it is inserted into the list.

Fibonacci Search:
It is a comparison based technique that uses Fibonacci numbers to
search an element in a sorted array. It has a O(log n) time complexity.
• F(n) = F(n-1) + F(n-2), F(0) = 0, F(1) = 1 is way to define
fibonacci numbers recursively.
• First few Fibinacci Numbers are 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55,
89, 144, …

Let the element to be searched is x, then the idea is to first find the
smallest Fibonacci number that is greater than or equal to length of
given array. Let the Fibonacci number be fib(nth Fibonacci number).
Use (n-2)th Fibonacci number as index and say it is i, then compare
a[i] with x, if x is same then return i. Else if x is greater, then search the
sub array after i, else search the sub array before i.

# Python3 program for Fibonacci search.

from bisect import bisect_left


# Returns index of x if present, else returns -1

def fibMonaccianSearch(arr, x, n):


# Initialize fibonacci numbers
fibMMm2 = 0 # (m-2)'th Fibonacci No.
fibMMm1 = 1 # (m-1)'th Fibonacci No.
fibM = fibMMm2 + fibMMm1 # m'th Fibonacci
# fibM is going to store the smallest
# Fibonacci Number greater than or equal to n
while (fibM < n):
fibMMm2 = fibMMm1
fibMMm1 = fibM
fibM = fibMMm2 + fibMMm1

Second Level Students 2021/2022 49


Data Structures and Algorithms with Python

# Marks the eliminated range from front


offset = -1;
# while there are elements to be inspected.
# Note that we compare arr[fibMm2] with x.
# When fibM becomes 1, fibMm2 becomes 0
while (fibM > 1):
# Check if fibMm2 is a valid location
i = min(offset+fibMMm2, n-1)
# If x is greater than the value at
# index fibMm2, cut the subarray array
# from offset to i
if (arr[i] < x):
fibM = fibMMm1
fibMMm1 = fibMMm2
fibMMm2 = fibM - fibMMm1
offset = i

# If x is greater than the value at


# index fibMm2, cut the subarray
# after i+1
elif (arr[i] > x):
fibM = fibMMm2
fibMMm1 = fibMMm1 - fibMMm2
fibMMm2 = fibM - fibMMm1
# element found. return index
else :
return i
# comparing the last element with x */
if(fibMMm1 and arr[offset+1] == x):
return offset+1;
# element not found. return -1
return -1

# call
arr = [10, 22, 35, 40, 45, 50, 80, 82, 85, 90, 100]
n = len(arr)
x = 80
print("Found at index:", fibMonaccianSearch(arr, x, n))

Second Level Students 2021/2022 50


Data Structures and Algorithms with Python

Time complexity for Fibonacci search is O(log2 n).

Jump Search:
▪ For sorted arrays
▪ To Check fewer elements (than linear search) by jumping fixed
steps
▪ Or skipping some elements
Jump Search Algorithm
▪ Step 1 Calculate Jump size.
▪ Step 2 Jump from index i to index i+jump
▪ Step 3 If x== arr[i+jump] return x
Else jump back a step
▪ Step 4 Perform linear search
Example:
➢ Assume the following sorted array:
➢ Search for 77

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
❑ Calculate :

▪ Size of array n=16

▪ sqrt(n)=4 ➔ Jump size= 3

▪ Jump size =3

▪ Search from index 0

▪ Compare index value with search number 0<77

Second Level Students 2021/2022 51


Data Structures and Algorithms with Python

❑ Jump size =3

❑ Jump from index 0 to index 3 i-> i+jump 0+3=3

❑ Compare index value with search number 2<77

Arr[3]<77

❑ Jump size =3

❑ Jump from index 3 to index 6.

❑ Compare index value with search number 8<77

Arr[6]<77

❑ Jump size =3

❑ Jump from index 6 to index 9.

❑ Compare index value with search number 34<77

❑ Jump size =3

❑ Jump from index 9 to index 12

❑ Compare index value with search number 89>77

❑ Jump back a step.

Second Level Students 2021/2022 52


Data Structures and Algorithms with Python

❑ Perform linear search.

❑ Compare found at index 11.

Time Complexity: O(sqrt(n))

Second Level Students 2021/2022 53


Data Structures and Algorithms with Python

Basic Sort Algorithms

What are sort algorithms ?

Sorting in general refers to various methods of arranging or ordering


things based on criteria's (numerical, chronological, alphabetical,
hierarchical etc.). There are many approaches to sorting data and
each has its own merits and demerits.

def swap(lyst, i, j):


"""Exchanges the items at positions i and j."""
# You could say lyst[i], lyst[j] = lyst[j], lyst[i]
# but the following code shows what is really going on
temp = lyst[i]
lyst[i] = lyst[j]
lyst[j] = temp

Selection Sort
Perhaps the simplest strategy is to search the entire list for the
position of the smallest item.
If that position does not equal the first position, the algorithm swaps
the items at those positions. The algorithm then returns to the
second position and repeats this process, swapping the smallest item
with the item at the second position, if necessary. When the
algorithm reaches the last position in the overall process, the list is
sorted. The algorithm is called selection sort because each pass
through the main loop selects a single item to be moved.
Fig. 5-3 shows the states of a list of five items after each search and
swap pass of a selection sort. The two items just swapped on each
pass have asterisks next to them, and the sorted portion of the list is
shaded.

Second Level Students 2021/2022 54


Data Structures and Algorithms with Python

Unsorted List After 1st Pass After 2nd Pass After 3rd pass After 4th pass
5 1* 1 1 1
3 3 2* 2 2
1 5* 5 3* 3
2 2 3* 5* 4*
4 4 4 4 5*
Fig. 5-3 Data during Selection sort

Selection Sort Algorithm:

Here is the Python function for a selection sort:


def selectionSort(lyst):
i=0
while i < len(lyst) - 1: # Do n - 1 searches
minIndex = i # for the smallest
j=i+1
while j < len(lyst): # Start a search
if lyst[j] < lyst[minIndex]:
minIndex = j
j += 1
if minIndex != i: # Exchange if needed
swap(lyst, minIndex, i)
i += 1

Second Level Students 2021/2022 55


Data Structures and Algorithms with Python

This function includes a nested loop. For a list of size n, the outer
loop executes n- 1 times. On the first pass through the outer loop, the
inner loop executes n -1 times. On the
second pass through the outer loop, the inner loop executes n -2
times. On the last pass through the outer loop, the inner loop
executes once. Thus, the total number of comparisons for a list of
size n is the following:
(n-1)+ (n-2) + ….+1= n(n-1)/2= ½ n2 -1/2 n
Time Complexity O(n2)

# Python program for implementation of Selection Sort


import sys
A = [64, 25, 12, 22, 11]
# Traverse through all array elements
for i in range(len(A)):
# Find the minimum element in remaining
# unsorted array
min_idx = i
for j in range(i+1, len(A)):
if A[min_idx] > A[j]:
min_idx = j
# Swap the found minimum element with
# the first element
A[i], A[min_idx] = A[min_idx], A[i]
# Driver code to test above
print ("Sorted array")
for i in range(len(A)):
print("%d" %A[i])
Output:
Enter array size: 6
Enter the elements: 96 94 81 56 76 45
The elements after sorting are: 45 56 76 81 94 96

Second Level Students 2021/2022 56


Data Structures and Algorithms with Python

Example:
Use the selection sort to order the following integers:
64 25 12 22 11

Solution:
64 25 12 22 11
11 25 12 22 64
11 12 25 22 64
11 12 22 25 64
11 12 22 25 64

Bubble Sort
Another sort algorithm that is relatively easy to conceive and code is
called a bubble sort.
Its strategy is to start at the beginning of the list and compare pairs of
data items as it moves down to the end. Each time the items in the
pair are out of order, the algorithm swaps them.
This process has the effect of bubbling the largest items to the end of
the list. The algorithm then repeats the process from the beginning of
the list and goes to the next-to-last item, and so on, until it begins
with the last item. At that point, the list is sorted.

Unsorted List After 1st Pass After 2nd Pass After 3rd pass After 4th pass
5 4* 4 4 4
4 5* 2* 2 2
2 2 5* 1* 1
1 1 1 5* 3*
3 3 3 3 5*
Fig. 5-4 Data during Bubble sort

Bubble Sort Algorithm:


Step 1: Repeat Steps 2 and 3 for i=1 to 10
Step 2: Set j=1

Second Level Students 2021/2022 57


Data Structures and Algorithms with Python

Step 3: Repeat while j<=n


(A) if a[i] < a[j]
Then interchange a[i] and a[j]
[End of if]
(B) Set j = j+1
[End of Inner Loop]
[End of Step 1 Outer Loop]
Step 4: Exit

Second Level Students 2021/2022 58


Data Structures and Algorithms with Python

Here is the Python function for a bubble sort:

def bubbleSort(lyst):
n = len(lyst)
while n > 1: # Do n - 1 bubbles
i = 1 # Start each bubble

Second Level Students 2021/2022 59


Data Structures and Algorithms with Python

while i < n:
if lyst[i] < lyst[i - 1]: # Exchange if needed
swap(lyst, i, i - 1)
i += 1
n -= 1

OR
def bubbleSort(arr):
n = len(arr)
# Traverse through all array elements
for i in range(n):
# Last i elements are already in place
for j in range(0, n-i-1):
# traverse the array from 0 to n-i-1
# Swap if the element found is greater
# than the next element
if arr[j] > arr[j+1] :
arr[j], arr[j+1] = arr[j+1], arr[j]

# call
arr = [64, 34, 25, 12, 22, 11, 90]
bubbleSort(arr)
print ("Sorted array is:")
for i in range(len(arr)):
print ("%d" %arr[i])

Consider the list 5 1 4 2 8

First Pass:
(51428) ( 1 5 4 2 8 ), Here, algorithm compares the first two elements, and
swaps since 5 > 1.
(15428) ( 1 4 5 2 8 ), Swap since 5 > 4
(14528) ( 1 4 2 5 8 ), Swap since 5 > 2
Second Level Students 2021/2022 60
Data Structures and Algorithms with Python
(14258)
( 1 4 2 5 8 ), Now, since these elements are already in order (8 > 5), algorithm does not
swap them.
Second Pass:
(14258) (14258)
(14258) ( 1 2 4 5 8 ), Swap since 4 > 2
(12458) (12458)
(12458) (12458)
Now, the array is already sorted, but our algorithm does not know if it
is completed. The algorithm needs one whole pass without any swap
to know it is sorted.
Third Pass:
(12458) (12458)
(12458) (12458)
(12458) (12458)
(12458) (12458)

As with the selection sort, a bubble sort has a nested loop.


Time Complexity:
If an array containing n data items, then the outer loop executes n-1
times as the algorithm requires n-1 passes. In the first pass, the inner
loop is executed n-1 times; in the second pass, n-2 times; in the third
pass, n-3 times and so on. The total number of iterations resulting in
a run time of O(n2).
• Worst Case Performance O(n2)
• Best Case Performance O(n2)
• Average Case Performance O(n2)

You can make a minor adjustment to the bubble sort to improve its
best-case performance to linear. If no swaps occur during a pass
through the main loop, then the list is sorted. This can happen on any
pass and in the best case will happen on the first pass. You can track
the presence of swapping with a Boolean flag and return from the
function when the inner loop does not set this flag.

Second Level Students 2021/2022 61


Data Structures and Algorithms with Python

Here is the modified bubble sort function:

def bubbleSortWithTweak(lyst):
n = len(lyst)
while n > 1:
swapped = False
i=1
while i < n:
if lyst[i] < lyst[i - 1]: # Exchange if needed
swap(lyst, i, i - 1)
swapped = True
i += 1
if not swapped: return # Return if no swaps
n -= 1
Note that this modification only improves best-case behavior. On the
average, the behavior of this version of bubble sort is still O(n2 ).

Insertion Sort
Our modified bubble sort performs better than a selection sort for
lists that are already sorted. But our modified bubble sort can still
perform poorly if many items are out of order in the list. Another
algorithm, called an insertion sort, attempts to exploit the partial
Ordering of the list in a different way. It builds the sorted sequence
one number at a time. This is a suitable sorting technique in playing
card games.
Insertion sort provides several advantages:
✓ Simple implementation
✓ Efficient for (quite) small data sets
✓ Adaptive (i.e., efficient) for data sets that are already
substantially sorted: the time complexity is O(n + d), where d is
the number of inversions.
✓ More efficient in practice than most other simple quadratic
(i.e., O(n2)) algorithms such as selection sort or bubble sort; the
best case (nearly sorted input) is O(n)
✓ Stable; i.e., does not change the relative order of elements
with equal keys
Second Level Students 2021/2022 62
Data Structures and Algorithms with Python

✓ In-place; i.e., only requires a constant amount O(1) of


additional memory space.

The strategy is as follows:

✓ On the ith pass through the list, where i ranges from 1 to n - 1,


the ith item should be inserted into its proper place among the
first i items in the list.
✓ After the ith pass, the first i items should be in sorted order.
✓ This process is analogous to the way in which many people
organize playing cards in their hands. That is, if you hold the
first i - 1 cards in order, you pick the ith card and compare it to
these cards until its proper spot is found.
✓ As with our other sort algorithms, insertion sort consists of two
loops. The outer loop traverses the positions from 1 to n - 1. For
each position i in this loop, you save the item and start the
inner loop at position i -1. For each position j in this loop, you
move the item to position j + 1 until you find the insertion point
for the saved (ith) item.

Second Level Students 2021/2022 63


Data Structures and Algorithms with Python

Here is the code for the insertionSort function:


def insertionSort(lyst):
i=1
while i < len(lyst):
itemToInsert = lyst[i]
j=i-1
while j >= 0:
if itemToInsert < lyst[j]:
lyst[j + 1] = lyst[j]
j -= 1
else:
break
lyst[j + 1] = itemToInsert
i += 1

OR
# Python program for implementation of Insertion Sort
def insertionSort(arr):
# Traverse through 1 to len(arr)
for i in range(1, len(arr)):

Second Level Students 2021/2022 64


Data Structures and Algorithms with Python

key = arr[i]
# Move elements of arr[0..i-1], that are
# greater than key, to one position ahead
# of their current position
j = i-1
while j >=0 and key < arr[j] :
arr[j+1] = arr[j]
j -= 1
arr[j+1] = key

# Call
arr = [12, 11, 13, 5, 6]
insertionSort(arr)
print ("Sorted array is:")
for i in range(len(arr)):
print ("%d" %arr[i])

Second Level Students 2021/2022 65


Data Structures and Algorithms with Python

Time Complexity:
✓ Worst Case Performance O(n2)
✓ Best Case Performance(nearly) O(n)
✓ Average Case Performance O(n2)

The analysis focuses on the nested loop. The outer loop executes n −
1 times. In the worst case, when all the data are out of order, the
inner loop iterates once on the first pass through the outer loop,
twice on the second pass, and so on, for a total of ½ n2- ½ n times.
Thus, the worst-case behavior of insertion sort is O(n2 ).

The more items in the list that are in order, the better insertion sort
gets until, in the best case of a sorted list, the sort’s behavior is linear.
In the average case, however, insertion sort is still quadratic.
Fig. 5-4 shows the states of a list of five items after each pass through
the outer loop of an insertion sort. The item to be inserted on the
next pass is marked with an arrow; after it is inserted, this item is
marked with an asterisk.

Unsorted List After 1st Pass After 2nd Pass After 3rd pass After 4th pass
2 2 1* 1 1
5 5 (no insertion) 2 2 2
1 1  5 4* 3*
4 4 4 5 4
3 3 3 3 5
Fig. 5-4 Data during Insertion sort

Second Level Students 2021/2022 66


Data Structures and Algorithms with Python

Q: Explain why the modified bubble sort still exhibits O(n2 )


behavior on the average.

Faster Sorting
The three sort algorithms considered thus far have O(n2 ) running
times. There are several variations on these sort algorithms, some of
which are marginally faster, but they, too, are O(n2 ) in the worst and
average cases. However, you can take advantage of some better
algorithms that are O(n log n). The secret to these better algorithms is
a divide-and-conquer strategy. That is, each algorithm finds a way of
breaking the list into smaller sublists.
These sublists are then sorted recursively. Ideally, if the number of
these subdivisions is log(n) and the amount of work needed to
rearrange the data on each subdivision is n, then the total complexity
of such a sort algorithm is O(n log n). In Table 5-1, you can see that
the growth rate of work of an O(n log n) algorithm is much slower
than that of an O(n2 ) algorithm.

Table 5-1 Comparing n log n and n2

Second Level Students 2021/2022 67


Data Structures and Algorithms with Python

Quick sort

Quick sort is a divide and conquer algorithm. Quick sort first divides a
large list into two smaller sublists: the low elements and the high
elements. Quick sort can then recursively sort the sub-lists.

The steps are:

1. Pick an element, called a pivot, from the list.


2. Reorder the list so that all elements with values less than the pivot
come before the pivot, while all elements with values greater than
the pivot come after it (equal values can go either way).
After this partitioning, the pivot is in its final position. This is called
the partition operation.
3. Recursively apply the above steps to the sub-list of elements with
smaller values and separately the sub-list of elements with greater
values.

➢ Quick sort, or partition-exchange sort, is a sorting algorithm,


on average, makes O(n log n) comparisons to sort n items. In
the worst case, it makes O(n2) comparisons, though this
behavior is rare.

➢ Quick sort is often faster in practice than other O(n log n)


algorithms. It works firstly by partitioning the array around a
pivot value and then dealing with the 2 smaller partitions
separately.

Second Level Students 2021/2022 68


Data Structures and Algorithms with Python

➢ Partitioning is the most complex part of quick sort. The


simplest thing is to use the first value in the array, a[l] (or a[0]
as l = 0 to begin with) as the pivot.
After the partitioning, all values to the left of the pivot are <=
pivot and all values to the right are > pivot. The same
procedure for the two remaining sub lists is repeated and so on
recursively until we have the entire list sorted.

Advantages:
- One of the fastest algorithms on average.
- Does not need additional memory (the sorting takes place in
the array - this is called in-place processing).

Disadvantages: The worst-case complexity is O(n2)

Second Level Students 2021/2022 69


Data Structures and Algorithms with Python

Example:
Figure 5-5 illustrates these steps as applied to the numbers 12 19 17
18 14 11 15 13 16.
step Action carried out List state after the action
Let the list with pivot 14
1 Swap the pivot with the last item
2 Establish the boundary before the
first item.
3 Scan for the first item less than the
pivot.
4 Swap this item with the first item
after the boundary. In this example
the item swapped with itself.
5 Advance the boundary.
6 Scan for the next item less than the
pivot.
7 Swap this item with the first item
after the boundary
8 Advance the boundary.
9 Scan for the next item less than the
privot
10 Swap this item with the first item
after the boundary
11 Advance the boundary
12 Scan for the next item less than the
pivot (no one)
13 Interchange the pivot with the first
item after the boundary

Fig. 5-5 Quick sort

Second Level Students 2021/2022 70


Data Structures and Algorithms with Python

Complexity Analysis of Quicksort


During the first partition operation, you scan all the items from the
beginning of the list to its end. Thus, the amount of work during this
operation is proportional to n, the list’s length.

Partitioning a sublist

The amount of work after this partition is proportional to the left


sublist’s length plus the right sublist’s length, which together yield n -
1. And when these sublists are divided, there are four pieces whose
combined length is approximately n, so the combined work is
proportional to n yet again. As the list is divided into more pieces, the
total work remains proportional to n.
To complete the analysis, you need to determine how many times
the lists are partitioned.
Make the optimistic assumption that, each time, the dividing line
between the new sublists turns out to be as close to the center of the
current sublist as possible. In practice, this is not usually the case.
You already know from the discussion of the binary search algorithm
that when you divide a list in half repeatedly, you arrive at a single
element in about log2 n steps.
Thus, the algorithm is O(n log n) in the best-case performance.

Second Level Students 2021/2022 71


Data Structures and Algorithms with Python

For the worst-case performance, consider the case of a list that is


already sorted. If the pivot element chosen is the first element, then
there are n -1 elements to its right on the first partition, n - 2 elements
to its right on the second partition, and so on, as shown in Fig. 5-6.

Fig. 5-6 Worst case for quick sort

Although no elements are exchanged, the total number of partitions


is n - 1 and the total number of comparisons performed is ½ n2 -1/2 n
, the same number as in selection sort and bubble sort. Thus, in the
worst case, the quicksort algorithm is O(n2 ).
If you implement a quicksort as a recursive algorithm, your analysis
must also consider memory usage for the call stack. Each recursive
call requires a constant amount of memory for a stack frame, and
there are two recursive calls after each partition. Thus, memory
usage is O(log n) in the best case and O(n) in the worst case.
Although the worst-case performance of quicksort is rare,
programmers certainly prefer to avoid it. Choosing the pivot at the
first or last position is not a wise strategy. Other methods of choosing
the pivot, such as selecting a random position or choosing the

Second Level Students 2021/2022 72


Data Structures and Algorithms with Python

median of the first, middle, and last elements, can help to


approximate O(n log n) performance in the average case.

Implementation of Quicksort
The quicksort algorithm is most easily coded using a recursive
approach. The following script runs quicksort on a list of 20
randomly ordered integers.

# Python program for implementation of Quicksort Sort


# This function takes last element as pivot, places the pivot element at its correct
# position in sorted array, and places all smaller (smaller than pivot)
# to left of pivot and all greater elements to right of pivot

def partition(arr,low,high):
i = ( low-1 ) # index of smaller element
pivot = arr[high] # pivot
for j in range(low , high):
# If current element is smaller than or
# equal to pivot
if arr[j] <= pivot:
# increment index of smaller element
i = i+1
arr[i],arr[j] = arr[j],arr[i]
arr[i+1],arr[high] = arr[high],arr[i+1]
return ( i+1 )
# The main function that implements QuickSort
# arr[] --> Array to be sorted,
# low --> Starting index,
# high --> Ending index

# Function to do Quick sort


def quickSort(arr,low,high):
if low < high:

Second Level Students 2021/2022 73


Data Structures and Algorithms with Python

# pi is partitioning index, arr[p] is now at right place

pi = partition(arr,low,high)

# Separately sort elements before partition and after partition

quickSort(arr, low, pi-1)


quickSort(arr, pi+1, high)

# call
arr = [10, 7, 8, 9, 1, 5]
n = len(arr)
quickSort(arr,0,n-1)
print ("Sorted array is:")
for i in range(n):
print ("%d" %arr[i])

Example:

Second Level Students 2021/2022 74


Data Structures and Algorithms with Python

Time Complexity:
➢ Worst Case Performance O(n2)
➢ Best Case Performance(nearly) O(n log2 n)
➢ Average Case Performance O(n log2 n)

Second Level Students 2021/2022 75


Data Structures and Algorithms with Python

Merge Sort
Merge sort is based on Divide and conquer method. It takes the list to
be sorted and divide it in half to create two unsorted lists. The two
unsorted lists are then sorted and merged to get a sorted list. The
two unsorted lists are sorted by continually calling the merge-sort
algorithm; we eventually get a list of size 1 which is already sorted.
The two lists of size 1 are then merged.

This works as follows :


1. Divide the input which we have to sort into two parts in the middle.
Call it the left part and right part.
2. Sort each of them separately. Note that here sort does not mean to
sort it using some other method.
We use the same function recursively.
3. Then merge the two sorted parts.

Input the total number of elements that are there in an array


(number_of_elements).
Input the array (array[number_of_elements]). Then call the function
MergeSort() to sort the input array.
MergeSort() function sorts the array in the range [left,right] i.e. from
index left to index right inclusive.
Merge() function merges the two sorted parts. Sorted parts will be
from [left, mid] and [mid+1, right].
After merging output the sorted array.
Two Python functions collaborate in this design strategy:
•• mergeSort—The function called by users.
•• merge—A function that implements the merging process.

Second Level Students 2021/2022 76


Data Structures and Algorithms with Python

Implementing the Merging Process


MergeSort() function:
It takes the array, left-most and right-most index of the array to be
sorted as arguments.
Middle index (mid) of the array is calculated as (left + right)/2.
Check if (left<right) cause we have to sort only when left<right
because when left=right it is anyhow sorted.
Sort the left part by calling MergeSort() function again over the left
part MergeSort(array,left,mid) and the right part by recursive call of
MergeSort function as MergeSort(array,mid + 1, right).
Lastly merge the two arrays using the Merge function.

Merge() function:
It takes the array, left-most , middle and right-most index of the array
to be merged as arguments.
Finally copy back the sorted array to the original array.

# Recursive Python Program for merge sort


def merge(left, right):
if not len(left) or not len(right):
return left or right
result = []
i, j = 0, 0
while (len(result) < len(left) + len(right)):
if left[i] < right[j]:
result.append(left[i])
i+= 1
else:
result.append(right[j])
j+= 1
if i == len(left) or j == len(right):
result.extend(left[i:] or right[j:])

Second Level Students 2021/2022 77


Data Structures and Algorithms with Python

break
return result
def mergesort(list):
if len(list) < 2:
return list
middle = int(len(list)/2)
left = mergesort(list[:middle])
right = mergesort(list[middle:])

return merge(left, right)

# Call
seq = [12, 11, 13, 5, 6, 7]
print("Given array is")
print(seq);
print("\n")
print("Sorted array is")
print(mergesort(seq))

OR

from arrays import Array

def mergeSort(lyst):

# lyst list being sorted


# copyBuffer temporary space needed during merge
copyBuffer = Array(len(lyst))
mergeSortHelper(lyst, copyBuffer, 0, len(lyst) - 1)
def mergeSortHelper(lyst, copyBuffer, low, high):
# lyst list being sorted
# copyBuffer temp space needed during merge
# low, high bounds of sublist
# middle midpoint of sublist
if low < high:
middle = (low + high) // 2
mergeSortHelper(lyst, copyBuffer, low, middle)

Second Level Students 2021/2022 78


Data Structures and Algorithms with Python

mergeSortHelper(lyst, copyBuffer, middle + 1, high)


merge(lyst, copyBuffer, low, middle, high)

Figure 5-7 shows the sublists generated during recursive calls to


mergeSortHelper, starting from a list of eight items. Note that, in this
example, the sublists are evenly subdivided at each level and there
are 2k sublists to be merged at level k. Had the length of the initial list
not been a power of two, then an exactly even subdivision would not
have been achieved at each level and the last level would not have
contained a full complement of sublists. Fig. 5-8 traces the process of
merging the sublists generated in Fig. 5-7.

Fig. 5-7 mergeSortHelper function calls results

Fig. 5-8 Merging the sublists generated during Merge sort

Second Level Students 2021/2022 79


Data Structures and Algorithms with Python

Finally, here is the code for the merge function:

def merge(lyst, copyBuffer, low, middle, high):


# lyst list that is being sorted
# copyBuffer temp space needed during the merge process
# low beginning of first sorted sublist
# middle end of first sorted sublist
# middle + 1 beginning of second sorted sublist
# high end of second sorted sublist
# Initialize i1 and i2 to the first items in each sublist
i1 = low
i2 = middle + 1
# Interleave items from the sublists into the
# copyBuffer in such a way that order is maintained.
for i in range(low, high + 1):
if i1 > middle:
copyBuffer[i] = lyst[i2] # First sublist exhausted
i2 += 1
elif i2 > high:
copyBuffer[i] = lyst[i1] # Second sublist
exhausted
i1 += 1
elif lyst[i1] < lyst[i2]:
copyBuffer[i] = lyst[i1] # Item in first sublist <
i1 += 1
else:
copyBuffer[i] = lyst[i2] # Item in second sublist <
i2 += 1
for i in range (low, high + 1): # Copy sorted items back to
lyst[i] = copyBuffer[i] # proper position in lyst

The merge function combines two sorted sublists into a larger sorted
sublist. The first Sublist lies between low and middle and the second
between middle + 1 and high. The process consists of three steps:

Second Level Students 2021/2022 80


Data Structures and Algorithms with Python

1. Set up index pointers to the first items in each sublist. These are at
positions low and middle + 1.
2. Starting with the first item in each sublist, repeatedly compare
items. Copy the smaller item from its sublist to the copy buffer and
advance to the next item in the sublist. Repeat until all items have
been copied from both sublists. If the end of one sublist is reached
before the other’s, finish by copying the remaining items from the
other sublist.
3. Copy the portion of copyBuffer between low and high back to the
corresponding positions in lyst.

Time Complexity:
➢ Worst Case Performance O(n log2 n)
➢ Best Case Performance(nearly) O(n log2 n)
➢ Average Case Performance O(n log2 n)

The running time of the merge function is dominated by the two for
statements, each of which loops (high − low + 1) times.
Consequently, the function’s running time is O(high − low), and all
the merges at a single level take O(n) time. Because
mergeSortHelper splits sublists as evenly as possible at each level,
the number of levels is O(log n), and the maximum running time for
this function is O(n log n) in all cases.
The merge sort has two space requirements that depend on the list’s
size. First, O(log n) space is required on the call stack to support
recursive calls. Second, O(n) space is used by the copy buffer.

Second Level Students 2021/2022 81


Data Structures and Algorithms with Python

Questions:
1- Why is merge sort an O(n log n) algorithm in the worst case?
2- Describe the strategy of quicksort and explain why it can reduce the
time complexity of sorting from O(n2 ) to O(n log n).
3- Explain why quicksort is not O(n log n) in all cases? Describe the
worst-case situation for quicksort and give a list of 10 integers, that
would produce this behavior.
4- The partition operation in quicksort chooses the item at the midpoint
as the pivot. Describe two other strategies for selecting a pivot value.

Comparison of Sorting Algorithms

An Exponential Algorithm: Recursive Fibonacci


What is Fibonacci series? Describe a recursive Fibonacci function to
obtain a count of the recursive calls with various problem sizes. (the
number of calls seemed to grow much faster than the square of the
problem size.

Second Level Students 2021/2022 82


Data Structures and Algorithms with Python

def fib(n):
"""The recursive Fibonacci function."""
if n < 3:
return 1
else:
return fib(n - 1) + fib(n - 2)

Note that fib(4) requires only 4 recursive calls, which seems linear,
but fib(6) requires 2 calls of fib(4), among a total of 14 recursive
calls. Indeed, it gets much worse as the problem size grows.
Represent as balanced tree.
The number of recursive calls generally is 2n+1 -2 in fully balanced call
trees, where n is the argument at the top or the root of the call tree.
This is clearly the behavior of an exponential, O(kn) algorithm.
Although the bottom two levels of the call tree for recursive Fibonacci
are not completely filled in, its call tree is close enough in shape to a
fully balanced tree to rank recursive Fibonacci as an exponential
algorithm. The constant k for recursive Fibonacci is approximately
1.63.

Alternatively, recursive functions that are called repeatedly with the


same arguments, such as the Fibonacci function, can be made more
efficient by a technique called memoization.
According to this technique, the program maintains a table of the
values for each argument used with the function. Before the function
recursively computes a value for a given argument, it checks the
table to see if that argument already has a value. If so, that value is
simply returned.
If not, the computation proceeds and the argument and value are
added to the table afterward.

Second Level Students 2021/2022 83


Data Structures and Algorithms with Python

Converting Fibonacci to a Linear Algorithm


Although the recursive Fibonacci function reflects the simplicity and
elegance of the recursive definition of the Fibonacci sequence, the
run-time performance of this function is unacceptable. A different
algorithm improves on this performance by several orders of
magnitude and, in fact, reduces the complexity to linear time.

Fibonacci sequence 1, 1, 2, 3, 5, 8, 13, …..


The new algorithm starts a loop if n is at least the third Fibonacci
number (3 to n). The loop computes the sum and replacements.

The sum at the end of the loop is the nth Fibonacci number. Here is
the pseudocode for this algorithm:
Set sum to 1
Set first to 1
Set second to 1
Set count to 3
While count <= N
Set sum to first + second
Set first to second
Set second to sum
Increment count

The Python code:


def fib(n, counter):
"""Count the number of iterations in Fibonacci."""
sum = 1
first = 1
second = 1
count = 3
while count <= n:
counter.increment()
sum = first + second
first = second

Second Level Students 2021/2022 84


Data Structures and Algorithms with Python

second = sum
count += 1
return sum

Problem Size Iterations


2 0
4 2
8 6
16 14
32 30

Questions:
Q1) Choose the correct answer:
1- A binary search assumes that the data are:
a. Arranged in no particular order
b. Sorted
2- A selection sort makes at most:
a. n2 exchanges of data items b. n exchanges of data items
3- An example of an algorithm whose best-case, average-case, and worst-
case behaviors are the same is:
a. Sequential search
b. Selection sort
c. Quicksort
4- The recursive Fibonacci function makes approximately:
a. n2 recursive calls for problems of a large size n
b. 2n recursive calls for problems of a large size n

Q2) The list method reverse reverses the elements in the list. Define
a function named reverse that reverses the elements in its list
argument (without using the method reverse). Try to make this
function as efficient as possible, and state its computational
complexity using big-O notation.

Q3) Python’s pow function returns the result of raising a number to a


given power. Define a function expo that performs this task and state

Second Level Students 2021/2022 85


Data Structures and Algorithms with Python

its computational complexity using big-O notation. The first


argument of this function is the number, and the second argument is
the exponent (nonnegative numbers only).
You can use either a loop or a recursive function in your
implementation, but do not use Python’s ** operator or pow
function.

Q4) An alternative strategy for the expo function uses the following
recursive definition:
expo(number, exponent)
=1, when exponent=0
=number *expo(number, exponent-1), when exponent is
odd
=(expo(number, exponent / 2))2, when exponent is even
Define a recursive function expo that uses this strategy, and state its
Computational complexity using big-O notation.

Q5) The function makeRandomList creates and returns a list of


numbers of a given size (its argument). The numbers in the list are
unique and range from 1 through the size. They are placed in random
order. Here is the code for the function:
def makeRandomList(size):
lyst = []
for count in range(size):
while True:
number = random.randint(1, size)
if not number in lyst:
lyst.append(number)
break
return lyst
You can assume that range, randint, and append are constant time
functions. You can also assume that random.randint more rarely
returns duplicate numbers as the range between its arguments

Second Level Students 2021/2022 86


Data Structures and Algorithms with Python

increases. State the computational complexity of this function using


big-O notation, and justify your answer.

Q6) Modify the quicksort function so that it calls insertion sort to


sort any sublist whose size is less than 50 items. Compare the
performance of this version with that of the original one, using data
sets of 50, 500, and 5000 items. Then adjust the threshold for using
the insertion sort to determine an optimal setting.

Second Level Students 2021/2022 87


Data Structures and Algorithms with Python

4. Python Programming Introduction


This chapter gives a quick overview of Python programming.

Install Python 3 (then you have the prompt >>>) + editor


(Jupyter, VScode, …)

To save and edit our program in a file we need an editor. There


are lots of editors. For Windows you can use Notepad++. Of
course, you can also use an IDE (Integrated Development
Environment) like PyCharm, Vs code or Spyder for this purpose.
So, after you have found the editor or the IDE of your choice, you
are ready to develop your mini program (script).

4.1 Script and Module


If you quit from the Python interpreter and enter it again, the
definitions you have made (functions and variables) are lost.
Therefore, use a text editor better, this is known as creating
a script.

As your program gets longer, split it into several files for easier
maintenance. A Python program consists of one or more
modules. You may also want to use your own function that you
have written in several programs without copying its definition
into each program.

To support this, Python has a way to put definitions in a file and


use them in a script. Such a file is called a module; definitions

Second Level Students 2021/2022 88


Data Structures and Algorithms with Python

from a module can be imported into other modules or into


the main module.

A module is a file containing Python definitions and statements.


The file name is the module name with the suffix .py. Within a
module, the module’s name (as a string) is available as the value
of the global variable __name__.

So,

• Scripts are top level files intended for execution and


• Modules are intended to be imported

For instance, create a file called fibo.py in the current directory


with the following contents:

# Fibonacci numbers module

def fib(n): # write Fibonacci series up to n


a, b = 0, 1
while a < n:
print(a, end=' ')
a, b = b, a+b
print()

def fib2(n): # return Fibonacci series up to n


result = []
a, b = 0, 1
while a < n:
result.append(a)
a, b = b, a+b
return result

Import this module with the following command, in a script called


lec1 fibonacci.py
Second Level Students 2021/2022 89
Data Structures and Algorithms with Python

It only enters the module name fibo not the functions fib and
fib2. Use the module name to access the functions:

import fibo

fibo.fib(1000)
# out: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
print(fibo.fib2(100))
# out: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
print(fibo.__name__)
# out: fibo

# you can also use:


f = fibo.fib
print(f(500))
# out: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

Also, you can use:


from fibo import fib, fib2
print(fib(500))
# out: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

OR:
from fibo import *
print(fib(500))

Second Level Students 2021/2022 90


‫‪Data Structures and Algorithms with Python‬‬

‫?‪NOTE: What is Library‬‬

‫‪In computer science, a library is a collection of non-volatile‬‬


‫‪resources used by computer programs, often for software‬‬
‫‪development.‬‬ ‫‪These‬‬ ‫‪may‬‬ ‫‪include‬‬ ‫‪configuration‬‬ ‫‪data,‬‬
‫‪documentation, help data, message templates, pre-written code‬‬
‫‪and subroutines, classes, values, or type specifications.‬‬
‫في مصطلحات عالم البرمجة‪" ،‬المكتبة" هي مجموعة من اإلجراءات البرمجية المستقلة التي يمكن‬
‫استدعاؤها من قبل البرامج لتنفيذ وظائف تكمل عملها‪.‬‬

‫والمكونات البرمجية بين‬


‫ّ‬ ‫ويحتوي ملف المكتبة نصوصا برمجية‪ .‬هذا األمر يسمح بمشاركة نفس الوظائف‬
‫أكث ر من تطبيق‪ ،‬ويزيد من حرية القيام بتعديلها‪ ،‬لوجودها في ملفات خارجية منفصلة عن ملفات البرامج‬
‫األساسية‪ .‬أغلب المكتبات ال يمكنها العمل كبرامج تنفيذية‪ ،‬ولكن البعض منها ال يختلف عن البرامج التنفيذية‬
‫في شيء‪ ،‬فيتم تشغيلها بصورة ذاتية مستقلة‪ .‬كل من البرامج التنفيذية والمكتبات تصنع روابط مرجعية فيما‬
‫مكونات البرنامج‪ ،‬التي ينفذها برنامج الربط‪.‬‬
‫بينها‪ ،‬يتم ذلك في مرحلة تجميع ّ‬

‫مع حلول عام ‪ ،2009‬معظم برامج األنظمة الجديدة أصبحت تؤدي غالبية العمليات على النظام مستخدمة‬
‫ملفات المكتبات البرمجية‪ .‬تلك المكتبات أتاحت الوظائف التي تحتاج إليها التطبيقات الحديثة بحيث أصبح أكثر‬
‫األجزاء البرمجية لتلك التطبيقات مكتوبا في مكتبات النظام نفسه‪ .‬أول ما بدأنا البرمجة ‪ ،‬كنا نكتب كل الكود‬
‫في ملف واحد وبشكل موحد ‪ ،‬اي أنك تجد كل البرنامج في دالة واحدة ‪.‬‬

‫األمر كان عاديا في البداية ألننا لم نكتب سوى عمليات بسيطة ‪ ،‬لكنه صار مزعجا عندما كبرت المشكالت‬
‫المراد حلها وكبر حلها ‪ ،‬ويستدعي كتابة "كود" طويل يحوي الكثير من التفاصيل والعمليات ‪ ،‬وبالتالي أصبح‬
‫فهمه للقارئ صعبا ‪ ،‬وشكله مزعجا نوعا ما ‪ ،‬ناهيك عن نسبة التكرار الكبيرة لكثير من العمليات ‪.‬‬

‫لذا استدعى األمر عزل كل ما يمكن حسابه أو عمله على حدة في شكل "دوال" تختص كل منها بأداء مهمة‬
‫"واحدة" مخصصة ‪ ،‬وهذا ما كنا نسميه ب"نظام الوحدات" في البداية كنا نكتبها في نفس ملف البرنامج ‪،‬‬
‫ونستدعيها في الدالة الرئيسية للبرنامج (‪. )main function‬‬

‫والسؤال هنا كان كاآلتي ‪ :‬إذا احتجت إلحدى هذه الدوال في برنامج آخر ‪ ،‬فكيف أستخدمها دون الحاجة إلعادة‬
‫كتابتها مجددا ؟‬

‫هنا تأتي المكتبة ‪ ..‬أول مرة كتبت مكتبة كان بلغة "‪ . "C‬مفهومها كان بسيطا ‪َ :‬ج ِّ ّمع كل الدوال التي تظن أنك‬
‫ستحتاج الستخدامها أكثر من مرة ‪ ،‬واحفظها في هذا الملف ‪ ،‬الذي ستطلبه عند الحاجة ‪.‬‬

‫‪Second Level Students 2021/2022‬‬ ‫‪91‬‬


Data Structures and Algorithms with Python

4.2 Python Program Comments


A program comment is text ignored by the Python interpreter but
valuable to the reader. It begins with # extends to the end of the
current line.

# This is an end-of-line comment.

A multiline comment is a string enclosed in triple single


quotes or triple double quotes.

''' Program end '''


""" Program End """

Note
Python keywords and names are case-sensitive. Thus, while
keyword is not While.
Python keywords are spelled in lowercase letters and are color-
coded in orange in an IDLE window.

Type of Name Examples


Variable hoursWork, isAbsent, salary, age
Constant Absolute_Zero, Interest_Rate
Function/Method printResults, cubeRoot, isEmpty, area
Class BankAccount, SortedSet

Fig. 4-1 Examples of Python Naming Conventions


Note
Python special variables:

One of these special variables is __name__

Second Level Students 2021/2022 92


Data Structures and Algorithms with Python

When the interpreter runs a module, the __name__ variable will


be set as __main__ if the module that is being run is the main
program.

But if the code is importing the module from another module,


then the __name__ variable will be set to that module’s name.

if __name__ == “__main__”:

Do something

Literals and Keywords


In PL there are types of words or symbols used to construct

sentences.

As in all high-level programming languages, some of Python’s


basic symbols are keywords, such as if, while, and def, (orange
color). Also, there are identifiers (names), literals (numbers,
strings, and other built-in data structures), operators, and
delimiters (quotation marks, commas, parentheses, square
brackets, and braces). Among the identifiers are the names of
built-in functions, which are colored purple.

Numbers (integers, floating-point or complex numbers) are


written as they are in other programming languages. The
Boolean values True and False are keywords. Smart Python – no
need to declare data type, it detects it.

Second Level Students 2021/2022 93


Data Structures and Algorithms with Python

>>>type(2)
Class <int>
Some data structures, such as strings, tuples, lists, and
dictionaries, also have literals, as you will see later.

String Literals
You can enclose strings in single quotes, double quotes, or sets of
three double quotes or three single quotes (this notation is useful for a
string containing multiple lines) of text. Character values are single-
character strings.
The \ character is used to escape.

NOTE:

In python .....intendation matters, e.g.:

if a==1:

print("hey")

if a==2:
print("bye")

print("all the best")

but what about this code?


if a==2:
print("bye")
print("all the best")
then "all the best" will be printed only if a==2

def find_dt(x = int(input('enter a positive number '))):


if x < 0:

Second Level Students 2021/2022 94


Data Structures and Algorithms with Python

print ('not positive')


else:
div = ()
for i in range(1, x):
if x%i == 0:
div = div + (i,)
print (div)
Once the Python interpreter is started, you can issue any
command at the command prompt ">>>".
>>> hello

Error

>>> print(“Hello”)

>>>”Hello” print is not important with prompt

To end the interactive session: You can either use exit() or Ctrl-D
(i.e. EOF) . The brackets behind the exit function are crucial.

Second Level Students 2021/2022 95


Data Structures and Algorithms with Python

THE SHELL AS A SIMPLE CALCULATOR

Type an arithmetic expression:

>>> 2+2
>>>4.567 * 8.323 * 17
Python follows the usual order of operations in expressions. The
standard order of operations is expressed in the following
enumeration: (operator precedence)

1- exponents and roots


2- multiplication and division
3- addition and subtraction
In other words, no need for parentheses in the expression 3 + (2 * 4):

>>>3 + 2 * 4 # OUTPUT: 11

Variables

Values can be saved in variables.


maximal = 124
width = 94
print(maximal - width) # subtraction operator

# out: 30

# variables
balance=300 # integer
print(balance)
balance = balance + 10 # add 10 to balance
print(balance) # 310

<identifier> = <expression>

a, b= 1, 100 # two variables assigned values with the same = operator

a, b = b, a # swap a,b

Second Level Students 2021/2022 96


Data Structures and Algorithms with Python

value = min(100,
200) \ # escape operator
*3
print(value) # 300

from math import sqrt


print(sqrt(4))

from math import pi, sqrt


print(sqrt(4) * pi)

Example:

➢ So:

There are 9 important primitive data types – Boolean, Integer,


Long, Float, String, List, Tuple, Sets Dictionaries. (There are
more data types like complex, bytes, deque etc.).

Second Level Students 2021/2022 97


Data Structures and Algorithms with Python

Operators

Arithmetic operators (+, –, *, /, %).


The / operator produces a floating-point result with any numeric
operands, whereas the // operator produces an integer quotient.
The + operator means concatenation when used with collections,
such as strings and lists.
The ** operator is used for exponentiation.
The comparison operators <, <=, >, >=, ==, and != work with
numbers and strings.
The == operator compares the internal contents of data
structures, such as two lists.
Comparisons return True or False.
The logical operators and, or, and not .

Strings

Strings are created by putting a sequence of characters in quotes.


Strings can be surrounded by single quotes, double quotes, or triple
quotes, which are made up of three single or three double quotes.
Strings are immutable. In other words, once defined, they cannot be
changed.
"Hello" + " " + "World"
'Hello World'

Strings are immutable, once defined, they cannot be changed.

city = """
... Toronto is the largest city in Canada
Second Level Students 2021/2022 98
Data Structures and Algorithms with Python
... and the provincial capital of Ontario.
... It is located in Southern Ontario on the
... northwestern shore of Lake Ontario.
... """
print(city)

Toronto is the largest city in Canada


and the provincial capital of Ontario.
It is located in Southern Ontario on the
northwestern shore of Lake Ontario.

Multiplication on strings is defined, which is essentially a multiple


concatenation:
".-." * 4

OUTPUT: '.-..-..-..-.'

print("My first simple Python script!")


My first simple Python script!

and save it as my_first_ program.py.

Some operations with strings:


No. Expression Meaning
1 len(stringOne) Used to find the number of characters in
stringOne
2 “hello”+“world” Two strings are concatenated into a single
string
3 “world” == “hello” Equality can be tested using “==” sign
“world” == “world” 0 or False
1 or True
4 "hello"*3 Repetition "hellohellohello"
5 stringOne[0] Indexing “h”
6 stringOne[1:4] Slicing "ell"
7 “l” in stringOne Searching True

Second Level Students 2021/2022 99


Data Structures and Algorithms with Python

Functions
Define and Call. Python presents some standard functions like
print, input, abs, and min. Many other functions are available by
import from modules.
Example:
radius = float(input("Radius: ")) # conversion type
print("The area is", 3.14 * radius ** 2)

Function Arguments:
x=round(3.1674)
print(x) # out: 3
print("x=",x) # out: x=3

x=round(3.1674,1)
print("x=",x) # out: x=3.2

Arguments can be passed from one method to other using


parameters. When we call a function and pass a parameter to it.
A new variable is created which store the reference of the
passed object. Therefore, there is no way we can update the
variable in the calling function. The called function can reassign
its local variable to refer to a new object, but the variable in the
calling function will still refer to the old object.
Example:
Class Class1:
def __init__(self):
X=1
self.func(X)
print X
def func(self, Y):
Y=2

Second Level Students 2021/2022 100


Data Structures and Algorithms with Python

Example:
Class Class2:
def __init__(self):
X=[1, 2]
self.func(X)
print X
def func(self, Y):
Y.append(5)

Control Statements
if <Boolean expression>:
<sequence of statements>
if <Boolean expression>:
<sequence of statements>
else:
<sequence of statements>
if <Boolean expression>:
<sequence of statements>
elif <Boolean expression>:
<sequence of statements>
else:
<sequence of statements>

Loop Statements
while <Boolean expression>:
<sequence of statements>

Second Level Students 2021/2022 101


Data Structures and Algorithms with Python

Example:
# This code prints the product of the numbers from 1 to 10:
prod = 1
value = 1
while value <= 10:
prod *= value # same as prod= prod*value
value += 1
print(prod)

for <variable> in <iterable object>:


<sequence of statements>

Example:
# This code prints the product of the numbers from 1 to 10:
prod = 1
value = 1
for value in range(1,11):
prod *= value # same as prod= prod*value
value += 1
print(prod)

Second Level Students 2021/2022 102


Data Structures and Algorithms with Python

Exercises:
1. Write a program that takes the radius of a sphere (a floating-point
number) as input and outputs the sphere’s diameter, circumference,
surface area, and volume.
2. If π/4= 1-1/3+ 1/5- 1/7 + ……
Write a program that allows the user to specify the number of
iterations used in this approximation and displays the resulting value.
3. Write a program using a while loop that asks the user for a number,
and prints a countdown from that number to zero.
4. Write a program to generate Fibonacci numbers less than 500. The
Fibonacci sequence is generated by adding the previous two terms,
as 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
By considering the terms in the Fibonacci sequence whose values do
not exceed 1000, find the sum of the even-valued terms.

Second Level Students 2021/2022 103


Data Structures and Algorithms with Python

5. Collections
Chapter Objectives
After completing this chapter, you will be able to:
• Define the four general categories of collections—linear,
hierarchical, graph, and unordered.
• List the specific types of collections that belong to each of the
four categories of collections.
• Recognize which collections are appropriate for given
applications.
• Describe the commonly used operations on every type of
Collection.
• Describe the difference between an abstract collection type
and its implementations.

A collection (container data types) is a group of zero or more


items that can be treated as a conceptual unit. Nearly every
nontrivial piece of software involves the use of collections. Although
some of what you learn in computer science comes and goes with
changes in technology, the basic principles of organizing collections
endure.

Collection Types
Python includes several built-in collection types: the string,
the list, the tuple, the set, and the dictionary.
• list – It is a mutable container datatype and stores different
types of elements. It also allows duplicate because it follows
index-based storage. Using square brackets [].

Second Level Students 2021/2022 104


Data Structures and Algorithms with Python

• dict – It is mutable with key-value pair of storage. It stored the


values based on the key. Not using an index. Also not allows
duplicate keys. Using curly braces {}
• tuple – It is an immutable container datatype. Also stored
different type of elements and allows duplicates because it
also follows the index. Using braces ()
• set – It is mutable but it’s a kind of unordered collection it
doesn’t follow the index. Duplicates also not allowed.

These container data types are having some limitations during


implementation. For example, dict does not allow duplicate keys.
So that python provides some specialized data types to improve
the characteristics of these data types using collections module.
This module will provide an alternative for container data types.
The following are the specialized data structures to be used in the
collections module.
• OrderedDict

• DefaultDict

• Counter

• namedTuple

• deque

• chainMap

• UserDict

• UserList

• UserString

Note: 1- for using these above data structures you need to import
collections module.

Second Level Students 2021/2022 105


Data Structures and Algorithms with Python

2- normal dic: Each key is separated from its value by a colon (:), the
items are separated by commas, and the whole thing is enclosed in
curly braces. An empty dictionary without any items is written with
just two curly braces, like this: {}.
Keys are unique within a dictionary while values may not be. The
values of a dictionary can be of any type, but the keys must be of an
immutable data type such as strings, numbers, or tuples.

Example:
dict = {'Name': 'Ahmed', 'Age': 7, 'Class': 'First'}
print ("dict['Name']: ", dict['Name'])
print ("dict['Age']: ", dict['Age'])
output:
dict['Name']: Ahmed
dict['Age']: 7

Example:
dict = {'Name': 'Ahmed', 'Age': 7, 'Class': 'First'}

del dict['Name'] # remove entry with key 'Name'


dict.clear() # remove all entries in dict
del dict # delete entire dictionary

print ("dict['Age']: ", dict['Age'])


print ("dict['School']: ", dict['School'])

Example:
# normal dictionary test
dict = {'Name': 'Zara', 'Age': 7, 'Class': 'First'}

del dict['Name'] # remove entry with key 'Name'


print ("After del Name- dict['Age']: ", dict['Age'])

dict.clear() # remove all entries in dict


del dict # delete entire dictionary

print ("dict['Age']: ", dict['Age'])


print ("dict['School']: ", dict['School'])

Second Level Students 2021/2022 106


Data Structures and Algorithms with Python

Note Error when try to print dic after del.

#dic2.py
# create dic
squares = {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
print(squares.pop(4)) # remove a particular item out:16
print(squares) #out: {1: 1, 2: 4, 3: 9, 5: 25}
print(squares.popitem()) #will remove 5:25 out: (5,25)
print(squares) #out: {1: 1, 2: 4, 3: 9}
squares.clear() # remove all item
print(squares) # out: {}
del squares # delete the dic itself
print(squares) # Error: name 'squares' is not defined

Second Level Students 2021/2022 107


Data Structures and Algorithms with Python

Can be :

squares = {x: x*x for x in range(6)}

print(squares)

# output: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

Or:
squares = {}
for x in range(6)
squares[x]=x*x
print(squares)
------------------
#dict5.py
squares = {1: 1, 3: 9, 5: 25, 7: 49, 9: 81}
print(1 in squares) # out: True
print(2 not in squares) # out: True
print(49 in squares) #out: False
------------------
#dict6.py
squares = {1: 1, 3: 9, 5: 25, 7: 49, 9: 81}
for i in squares:
print(squares[i])
# out:
#1
#9
#25
#49
#81
------------------------
#dict7.py
s = {0: 'False', 1: 'False'}
print(all(s)) #False

s = {1: 'True', 2: 'True'}


print(all(s)) #True

s = {1: 'True', False: 0}


print(all(s)) #False

s = {} #True

Second Level Students 2021/2022 108


Data Structures and Algorithms with Python

print(all(s))

# 0 is False
# '0' is True
s = {'0': 'True'} #True
print(all(s))
------------------------
#dic10.py
# defining the dictionary
d = {'A' : [1, 2, 3, 4, 5, 6, 7, 8, 9], 'B' : 34,
'C' : 12,
'D' : [7, 8, 9, 6, 4] }
print(d)
#out:
# {'A': [1, 2, 3, 4, 5, 6, 7, 8, 9], 'B': 34, 'C': 12, 'D': [7, 8, 9,
6, 4]}
-------------------------

Data structures Classification:

➢ Primitive Data Structure


(Variables, arrays, pointers, structures, unions, etc. are examples of
primitive data structures.

➢ Non -Primitive Data Structure

Non-primitive DS

Linear List Non-Linear list

Array Linked list stack queue Graph Trees

Fig. 5-1 Non-primitive DS

Second Level Students 2021/2022 109


Data Structures and Algorithms with Python

The most commonly used operations on DS are :


• Create
• Adding
• Selection
• Updating
• Searching
• Sorting
• Merging
• Delete

So:
Python built-in data structures
Python comes with a general set of built-in data structures:
• lists
• tuples
• string
• dictionaries
• sets
• others...

Comparison between different DS

An algorithm which does N steps and algorithms which do N/2 or


3*N respectively are considered linear and approximately equally
efficient, because they perform several operations which is of the
same order.

Second Level Students 2021/2022 110


Data Structures and Algorithms with Python

A good data structures


• Efficient with user (some algorithm)
• Efficient with the amount of space used
• Efficient with the time it takes to perform some operations

Lists
Some of the list data type methods of list objects:

Mylist = range(5) [0,1,2,3,4]

len(Mylist) # 5 find the number of elements

list.append(x)
Add an item to the end of the list. Equivalent to a[len(a):] = [x].

list.extend(iterable)

Second Level Students 2021/2022 111


Data Structures and Algorithms with Python

Extend the list by appending all the items from the iterable.
Equivalent to a[len(a):] = iterable.

list.insert(i, x)

Insert an item at a given position. The first argument is the index of


the element before which to insert, so a.insert(0, x) inserts at the
front of the list, and a.insert(len(a), x) is equivalent
to a.append(x).

list.remove(x)

Remove the first item from the list whose value is equal to x. It raises
a ValueError if there is no such item.

list.pop([i])

Remove the item at the given position in the list, and return it. If no
index is specified, a.pop() removes and returns the last item in the
list. (The square brackets around the i in the method signature
denote that the parameter is optional, not that you should type
square brackets at that position.)

list.clear()

Remove all items from the list. Equivalent to del a[:].

list.index(x[, start[, end]])

Return zero-based index in the list of the first item whose value is
equal to x. Raises a ValueError if there is no such item.

The optional arguments start and end are interpreted as in the slice
notation and are used to limit the search to a particular subsequence
of the list. The returned index is computed relative to the beginning
of the full sequence rather than the start argument.

Second Level Students 2021/2022 112


Data Structures and Algorithms with Python

list.count(x)

Return the number of times x appears in the list.

list.sort(key=None, reverse=False)

Sort the items of the list in place (the arguments can be used for sort
customization, see sorted() for their explanation).

list.reverse()

Reverse the elements of the list in place.

list.copy()

Return a shallow copy of the list. Equivalent to a[:].

list2 = [5, 6]
print(mylist+list2) # [0, 1, 2, 3, 4, 5, 6] Concatenate to a single list
Mylist = Mylist*2 #Repeat [0, 1, 2, 3, 4, 0,1, 2, 3, 4]
Mylist [1:4] #Slicing
4 in Mylist # Searching True
Mylist.reverse() # [4,3,2,1,0]

Examples:
ff= [‘orange’, ‘apple’, ‘pear’, ‘banana’, ‘kiwi’, ‘apple’, ‘banana’]
ff.count(‘apple’)
ff.count(‘tangerine’) #0
ff.index(‘banana’)
ff.index(‘banana’, 4)# Find next banana starting a position 4
ff.reverse()
print(ff)
ff.append(‘grape’)

Second Level Students 2021/2022 113


Data Structures and Algorithms with Python

Notice that methods like insert, remove or sort that only


modify the list have no return value printed – they return the
default None. This is a design principle for all mutable data
structures in Python.

Tuple
A tuple is an immutable sequence of arbitrary data. Lists are created
by using parenthesis brackets consisting of values separated by
comma.
Cake=(‘C’, ‘a’, ‘k’, ‘e’)
print(type(Cake))

mixed_type=(‘c’, 0, 0, ‘a’, ‘k’, ‘e’)


for k in mixed_type:
print(k, “:”,type(k))

tup1=(1, 2, [5,6])

Dictionary
A Dictionary is a set of Key-Value pairs. Dictionaries are mutable.
Dictionaries are created using curly braces { }.

In dictionaries, if all keys (not values) are false or the dictionary is


empty, any() returns False. If at least one key is
true, any() returns True.

Second Level Students 2021/2022 114


Data Structures and Algorithms with Python

#dict8.py
#d = {0: 'False'} # 0 is False
#print(any(d)) #False
# 1 is True
#d = {0: 'False', 1: 'True'}
#print(any(d)) #True
# 0 and False are false
#d = {0: 'False', False: 0}
#print(any(d)) #False

# iterable is empty
#d = {}
#print(any(d)) #False
# 0 is False '0' is True
d = {'0': 'False'}
print(any(d)) #True
------------------------------
#dict8.py
testDict = {1: 'one', 2: 'two'}
print(testDict, 'length is', len(testDict)) #length is 2

testDict = {}
print(testDict, 'length is', len(testDict)) #length is 0
----------------------------------
#dict4.py
# Normal Dictionary example
odd_squares = {x: x*x for x in range(11) if x % 2 == 1}

print(odd_squares)
#output: {1: 1, 3: 9, 5: 25, 7: 49, 9: 81}
---------------------
#dicVowels.py
# vowels keys
keys = {'a', 'e', 'i', 'o', 'u' }
value = 'vowel'

vowels = dict.fromkeys(keys, value)


print(vowels)
# out: {'u': 'vowel', 'a': 'vowel', 'o': 'vowel', 'e': 'vowel', 'i': 'vowel'}
-----------------------------------
#dicVowels2.py

Second Level Students 2021/2022 115


Data Structures and Algorithms with Python

# vowels keys
keys = {'a', 'e', 'i', 'o', 'u' }
value = [1]

vowels = dict.fromkeys(keys, value)


print(vowels) # out: {'i': [1], 'o': [1], 'a': [1], 'u': [1], 'e': [1]
}

# updating the value


value.append(2)
print(vowels)
#out:{'i': [1, 2], 'o': [1, 2], 'a': [1, 2], 'u': [1, 2], 'e': [1, 2]}

------------------
#dic3.py
# Normal Dictionary example
marks = {}.fromkeys(['Math', 'English', 'Science'], 0)

# Output: {'Math': 0, 'English': 0, 'Science': 0}


print(marks)

for item in marks.items():


print(item)

# Output: ['English', 'Math', 'Science']


print(list(sorted(marks.keys())))

Replace dictionary value from another dictionary

Input : test_dict = {“Gfg” : 5, “is” : 8, “Best” : 10, “for” : 8, “Geeks” : 9},


updict = {“Geeks” : 10, “Best” : 17}
Output : {‘Gfg’: 5, ‘is’: 8, ‘Best’: 17, ‘for’: 8, ‘Geeks’: 10}
Explanation : “Geeks” and “Best” values updated to 10 and 17.

#dic14.py

Second Level Students 2021/2022 116


Data Structures and Algorithms with Python

test_dict = {"Gfg" : 5, "is" : 8, "Best" : 10, "for" : 8, "Geeks" : 9}


updict = {"Geeks" : 10, "Best" : 17}
print("Original dic:")
print(test_dict)
# output:
# Original dic:
#{'Gfg': 5, 'is': 8, 'Best': 10, 'for': 8, 'Geeks': 9}

for sub in test_dict:

# checking if key present in other dictionary


if sub in updict:
test_dict[sub] = updict[sub]
print("The updated dictionary: ")
print(test_dict)
#output:
# The updated dictionary:
# {'Gfg': 5, 'is': 8, 'Best': 17, 'for': 8, 'Geeks': 10}

Input : test_dict = {“Gfg” : 5, “is” : 8, “Best” : 10, “for” : 8, “Geeks” : 9},


updict = {“Geek” : 10, “Bet” : 17}
Output : {‘Gfg’: 5, ‘is’: 8, ‘Best’: 10, ‘for’: 8, ‘Geeks’: 9}
Explanation : No values matched, hence original dictionary.

1- OrderedDict
Simply called ordered collection of elements. Whereas a normal
dict object does not follow the order.
Example:
from collections import OrderedDict

name=OrderedDict()
name[0]=’b’
name[1]=’e’
name[2]=’s’
name[3]=’e’
name[4]=’n’
name[5]=’t’

Second Level Students 2021/2022 117


Data Structures and Algorithms with Python

print(“before:”, name)
name[3]=’a’
print(“after:”, name)

Output:

2- DefaultDict
DefaultDict is used to create a dict with duplicate keys. It is also a
subclass of dictionary using factory function to find missing values.
It is not like dict class because in normal dict class does not allow
using duplicate keys. If we try to use duplicate keys it will take the
last value of the particular key.
To overcome this limitation, use DefaultDict.

Example:

Second Level Students 2021/2022 118


Data Structures and Algorithms with Python

#dict10.py
from collections import defaultdict

marks=[('Andreu', 90), ('Ali', 95), ('Ashraf' ,99),('Bahaa', 98)]


dict_marks=defaultdict(list)
for k, v in marks:
dict_marks[k].append(v)
print(list(dict_marks.items()))
#out: [('Andreu', [90]), ('Ali', [95]), ('Ashraf', [99]), ('Bahaa', [9
8])]
------------------
#dict9.py
from collections import defaultdict

s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
d = defaultdict(list)
for k, v in s:
d[k].append(v)

d.items()
print(d) #out: {'yellow': [1, 3], 'blue': [2, 4], 'red': [1]})

3- counter
The counter is used to count the hashtable elements. It is also used
to count the items entered in the collection of a particular key. It is
also a subclass of the dictionary to count items.
It also performs some additional operations.
• element function – It returns the all elements of the counter in a list.

• Most_common() – It returns the count of each element in the

counter assorted list.


• Subtract() – It acquiring an Iterable Object as a parameter and

subtract the count of the elements in the available counter.

Example:
#dic12.py
from collections import Counter

marks=[('Andreu', 90), ('Ali', 95), ('Ashraf', 99),('Bahaa', 98)]

Second Level Students 2021/2022 119


Data Structures and Algorithms with Python

count_marks=Counter(name for name, marks in marks)


print(count_marks)
#output: Counter({'Andreu': 1, 'Ali': 1, 'Ashraf': 1, 'Bahaa': 1})

Example:
#dic12.py
from collections import Counter

a=[10,10,20,30,40,10, 20,50,40,60,10,30,60,70]
print(Counter(a))
#out:
#Counter({10: 4, 20: 2, 30: 2, 40: 2, 60: 2, 50: 1, 70: 1})

----------------------
#dic11.py
from collections import Counter

c=Counter(['a','b','c','a','b','a'])
print(c) #out:Counter({'a': 3, 'b': 2, 'c': 1})
print(c['a']) #out: 3
c1=Counter({'a':3,'b':2,'c':1})
print(c1['b']) #out: 2

Collections notes:

The string and the list are probably the most common and
fundamental types of collections. Other important types of
collections include stacks, queues, priority queues, binary search
trees, heaps, graphs, bags, and various types of sorted collections.
Collections can be homogeneous, meaning that all items in the
collection must be of the same type, or heterogeneous, meaning the
items can be of different types. In many programming languages,
collections are homogeneous, although most Python collections can
contain multiple types of objects.

Second Level Students 2021/2022 120


Data Structures and Algorithms with Python

Collections are typically dynamic rather than static, meaning they


can grow or shrink with the needs of a problem. Also, their contents
can change throughout the course of a program.
One exception to this rule is the immutable collection, such as
Python’s string or tuple. An immutable collection’s items are added
during its creation; after that, no items may be added, removed, or
replaced.
Another important distinguishing characteristic of collections is the
way they are organized. We now examine the organization used in
several broad categories of collections:

linear collections, hierarchical collections, graph collections,


unordered collections, and sorted collections.

Linear Collections
The items in a linear collection, like people in a line, are
ordered by position. Each item except the first has a unique
predecessor, and each item except the last has a unique successor.
As shown in Fig. 4-1, D2’s predecessor is D1, and D2’s successor
is D3.

Fig. 4-1 Linear Collection

Second Level Students 2021/2022 121


Data Structures and Algorithms with Python

Life examples of linear collections are grocery lists, stacks of


dinner plates, and a line of customers waiting at an ATM.

Hierarchical Collections
Data items in hierarchical collections are ordered in a
structure resembling an upsidedown-tree. Each data item except the
one at the top has just one predecessor, called its parent, but
potentially many successors, called its children. As shown in Figure 4-
2, D3’s predecessor (parent) is D1, and D3’s successors (children) are
D4, D5, and D6.

Fig. 4-2 Hierarchical Collection

A file directory system, a company’s organizational tree, and a


book’s table of contents are examples of hierarchical collections.

Graph Collections
A graph collection, or graph, is a collection in which each data
item can have many predecessors and many successors. As shown in
Fig. 4-3, all elements connected to D3 are considered to be both its

Second Level Students 2021/2022 122


Data Structures and Algorithms with Python

predecessors and its successors, and they are also called its
neighbors.

Fig. 4-3 Graph Collection

Examples of graphs are maps of airline routes between cities,


electrical wiring diagrams for buildings, and the world wide web.

Unordered Collections
As the name implies, items in an unordered collection are not in any
order, and it is not possible to meaningfully speak of an item’s
predecessor or successor. Fig. 4-4 shows such a structure.

Fig. 4-4 Unordered Collection

A bag of marbles is an example of an unordered collection. Although


you can put marbles into a bag and take marbles out of a bag in any

Second Level Students 2021/2022 123


Data Structures and Algorithms with Python

order you want, within the bag, the marbles are in no particular
order.

Sorted Collections
A sorted collection imposes a natural ordering on its items.
Examples are the entries in a phone book (the 20th century paper
variety) and the names on a class roster.
To impose a natural ordering, there must be some rule for comparing
items, such that itemi ≤ itemi+1, for the items visited in a sorted
collection.
Although a sorted list is the most common example of a sorted
collection, sorted collections need not be linear or ordered by
position. From the client’s perspective, sets, bags, and dictionaries
may be sorted, even though their items are not accessible by
position.
A special type of hierarchical collection, known as a binary search
tree, also imposes a natural ordering on its items.

A sorted collection allows the client to visit all its items in sorted
order. Some operations, such as searching, may be more efficient on
a sorted collection than on its unsorted cousin.

Second Level Students 2021/2022 124


Data Structures and Algorithms with Python

Fig. 4-5 Collection Types

Fundamental Operations on All Collection Types

Operation Category Description


Determine the size len function to obtain the number of items
currently in the collection.
Test item membership in operator to search for a given item in the
collection. Returns True/False
Traverse the collection for loop to visit each item in the collection
Obtain a string str to obtain the string representation of the
representation collection.
equality test == operator to determine if two collections
are equal (same type and same items)
Concatenate two + operator

Second Level Students 2021/2022 125


Data Structures and Algorithms with Python

collections
Convert to another Create a new collection with the same items
collection type (cloning is a special case)
Insert an item Add item
Remove an item
Replace an item
Access or retrieve an
item

➢ The operation name may be changed from one collection to


the other (add/insert/push - remove/del/pop).

Type Conversion

You can convert one type of collection to another type of collection.


Example:
>>> message = "Hi there!"
>>> lyst = list(message)
>>> lyst
['H', 'i', ' ', 't', 'h', 'e', 'r', 'e', '!']
>>> toople = tuple(lyst)
>>> toople
('H', 'i', ' ', 't', 'h', 'e', 'r', 'e', '!')

you can create a list from a range, as follows:


>>> lyst = list(range(1, 11, 2))
>>> lyst
[1, 3, 5, 7, 9]

Other functions, such as the dict function for dictionaries, expect


more specific types of iterable objects as arguments, such as a list
of (key, value) tuples.
Generally, if the argument is omitted, a collection’s type conversion
function returns a new, empty collection of that type.

Second Level Students 2021/2022 126


Data Structures and Algorithms with Python

A special case of type conversion is cloning, which returns an exact


copy of the argument to the conversion function. This should be the
case when the argument’s type is the same as the conversion
function.

Example:
To make a copy of a list and then compares the two lists using the is
and == operators. Because the two lists are not the same object, is
returns False. Because the two lists are distinct objects but are of the
same type and have the same structure (each pair of elements is the
same at each position in the two lists), == returns True.
>>> lyst1 = [2, 4, 8]
>>> lyst2 = list(lyst1)
>>> lyst1 is lyst2
False
>>> lyst1 == lyst2
True

Not only do the two lists in this example have the same structure, but
they share the same items. That is, the list function makes a shallow
copy of its argument list. These items are not themselves cloned
before being added to the new list; instead, mere references to these
objects are copied. This policy does not cause problems when the
items are immutable (numbers, strings, or Python tuples).

However, when collections share mutable items, side effects can


occur. To prevent these from happening, the programmer can create
a deep copy by writing a for loop over the source collection, which
explicitly clones its items before adding them to the new collection.

Second Level Students 2021/2022 127


Data Structures and Algorithms with Python

Iterators and Higher-Order Functions


Each type of collection supports an iterator or for loop, an operation
that iterates over the collection’s items.
The order in which the for loop serves up a collection’s items
depends on the way the collection is organized. For example, the
items in a list are visited by position, from first to last; the items in a
sorted collection are visited in ascending order, from smallest to
largest; and the items in a set or dictionary are visited in no particular
order.
The iterator is perhaps the most critical and powerful operation
provided by a collection. The for loop is used in many applications,
and it plays a useful role in the implementation of several other basic
collection operations, such as +, str, and type conversions, as well as
in several standard Python functions, such as sum, max, and min. As
you know, the sum, max, and min functions return the sum,
maximum, and minimum of a list of numbers, respectively. Because
these functions use a for loop in their implementations, they will
automatically work with any
other collection type, such as a set, a bag, or a tree, that also provides
a for loop.
The for loop or iterator also supports the use of the higher-order
functions map, filter, and reduce (introduced in Chapter 1). Each of
these functions expects another function and a collection as
arguments. Because all collections support a for loop, the map,
filter, and reduce functions can be used with any type of collection,
not just lists.

Second Level Students 2021/2022 128


Data Structures and Algorithms with Python

 Implementations of Collections
Naturally, programmers who work with programs that include
collections have a rather different perspective on those collections
than the programmers who are responsible for implementing them in
the first place.
Programmers who use collections need to know how to instantiate
and use each type of collection. From their perspective, a collection
is a means for storing and accessing data items in some
predetermined manner, without concern for the details of the
collection’s implementation.
In other words, from a user’s perspective, a collection is an
abstraction, and for this reason, in computer science, collections are
also called abstract data types (ADTs). The user of an ADT is
concerned only with learning its interface, or the set of operations
that objects of that type recognize.
Developers of collections, on the other hand, are concerned with
implementing a collection’s behavior in the most efficient manner
possible, with the goal of providing
the best performance to users of the collections. Numerous
implementations are usually possible. However, many of these take
so much space or run so slowly that they can be dismissed as
pointless. Those that remain tend to be based on several underlying
approaches to organizing and accessing computer memory.
Some programming languages, like Python, provide only one
implementation of each of the available collection types. Other
languages, like Java, provide several.
For example,
Java’s java.util package includes two implementations of lists,
named ArrayList and LinkedList; and two implementations of sets

Second Level Students 2021/2022 129


Data Structures and Algorithms with Python

and maps (like Python dictionaries), named HashSet, TreeSet,


HashMap, and TreeMap. Java programmers use the same interfaces
(set of operations) with each implementation but are free to choose
among implementations with respect to their performance
characteristics and other criteria.
In this book and for each category of collection (linear, hierarchical,
graph, unordered, sorted), you’ll see one or more abstract collection
types and one or more implementations of each type.
The idea of abstraction is not unique to a discussion of collections. It
is an important principle in many endeavors both in and out of
computer science. For example, when studying the effect of gravity
on a falling object, you might try to create an experimental situation
in which you can ignore incidental details such as the color and taste
of the object (for example, the sort of apple that hit Newton on the
head).
A house plan is an abstraction of the physical house that allows you
to focus on structural elements without being overwhelmed by
incidental details such as the color of the kitchen cabinets— details
that are important to the overall look of the completed house, but
not to the relationships among the house’s main parts.
In computer science, abstraction is used for ignoring or hiding details
that are, for the moment, nonessential. A software system is often
built layer by layer, with each layer treated as an abstraction or “ideal
type” by the layers above that utilize it. Without abstraction, you
would need to consider all aspects of a software system
simultaneously, which is an impossible task. Of course, you must
consider the details eventually, but you can do so in a small and
manageable context.
In Python, functions and methods are the smallest units of
abstraction, classes are the next in size, and modules are the largest.

Second Level Students 2021/2022 130


Data Structures and Algorithms with Python

This text implements abstract collection types as classes or sets of


related classes in modules.

4- Namedtuple():
Normal Tuple:
A Tuple in Python is like a list. The difference between tuple and list is
that we cannot change the elements of the tuple once it is assigned,
whereas the elements of a list can be changes.
#tuple1.py
my_tuple = () #empty tuple
print(my_tuple) # out: ()

my_tuple = (1, 2, 3) #tuple has integers


print(my_tuple) #out: (1,2,3)
my_tuple = (1, "Hello", 3.4)
print(my_tuple) #out: (1, 'Hello', 3.4)

# nested tuple
my_tuple = ("mouse", [8, 4, 6], (1, 2, 3))
print(my_tuple) #out: ('mouse', [8, 4, 6], (1, 2, 3))

A tuple can also be created without using parentheses. This is known


as tuple packing.
#tuple2.py
my_tuple = 3, 4.6, "dog"
print(my_tuple) # out:(3, 4.6, 'dog')

# tuple unpacking is also possible


a, b, c = my_tuple

print(a) # 3
print(b) # 4.6
print(c) # dog
----------------
#tuple3.py
# Accessing tuple elements using indexing
my_tuple = ('p','e','r','m','i','t')

Second Level Students 2021/2022 131


Data Structures and Algorithms with Python

print(my_tuple[0]) # 'p'
print(my_tuple[5]) # 't'

# IndexError: list index out of range


# print(my_tuple[6])

# Index must be an integer


# TypeError: list indices must be integers, not float
# my_tuple[2.0]

# nested tuple
n_tuple = ("mouse", [8, 4, 6], (1, 2, 3))

# nested index
print(n_tuple[0][3]) # 's'
print(n_tuple[1][1]) # 4
----------------------
#tuple4.py
# Negative indexing for accessing tuple elements
my_tuple = ('p', 'e', 'r', 'm', 'i', 't')

# Output: 't'
print(my_tuple[-1])

# Output: 'p'
print(my_tuple[-6])
# Accessing tuple elements using slicing
my_tuple = ('p','r','o','g','r','a','m','i','z')

# elements 2nd to 4th


# Output: ('r', 'o', 'g')
print(my_tuple[1:4])

# elements beginning to 2nd


# Output: ('p', 'r')
print(my_tuple[:-7])

# elements 8th to end


# Output: ('i', 'z')
print(my_tuple[7:])

# elements beginning to end

Second Level Students 2021/2022 132


Data Structures and Algorithms with Python

# Output: ('p', 'r', 'o', 'g', 'r', 'a', 'm', 'i', 'z')
print(my_tuple[:])
----------------------
Unlike lists, tuples are immutable (elements of a tuple cannot be
changed once they have been assigned). But, if the element is itself a
mutable data type like list, its nested items can be changed. We can
also assign a tuple to different values (reassignment).

#tuple5.py
# Changing tuple values
my_tuple = (4, 2, 3, [6, 5])

# TypeError: 'tuple' object does not support item assignment


# my_tuple[1] = 9

# However, item of mutable element can be changed


my_tuple[3][0] = 9 # Output: (4, 2, 3, [9, 5])
print(my_tuple)

# Tuples can be reassigned


my_tuple = ('p', 'r', 'o', 'g', 'r', 'a', 'm', 'i', 'z')

# Output: ('p', 'r', 'o', 'g', 'r', 'a', 'm', 'i', 'z')
print(my_tuple)
-----------
#tuple6.py
# Concatenation
# Output: (1, 2, 3, 4, 5, 6)
print((1, 2, 3) + (4, 5, 6))

# Repeat
# Output: ('Repeat', 'Repeat', 'Repeat')
print(("Repeat",) * 3)
y=("Repeat",) * 3
print(y) #out: ('Repeat', 'Repeat', 'Repeat')
---------------------
As discussed above, we cannot change the elements in a tuple. It
means that we cannot delete or remove items from a tuple.
Second Level Students 2021/2022 133
Data Structures and Algorithms with Python

Deleting a tuple entirely, however, is possible using the keyword del.


#tuple7.py
# Deleting tuples
my_tuple = ('p', 'r', 'o', 'g', 'r', 'a', 'm', 'i', 'z')
print(my_tuple)
# can't delete items
# TypeError: 'tuple' object doesn't support item deletion
# del my_tuple[3]

# Can delete an entire tuple


del my_tuple

# NameError: name 'my_tuple' is not defined


print(my_tuple)
-----------------
Methods with tuples:
#tuple8.py
my_tuple = ('a', 'p', 'p', 'l', 'e',)

print(my_tuple.count('p')) # Output: 2
print(my_tuple.index('l')) # Output: 3
----------------------
#tuple9.py
# Membership test in tuple
my_tuple = ('a', 'p', 'p', 'l', 'e',)

# In operation
print('a' in my_tuple) #True
print('b' in my_tuple) #False

# Not in operation
print('g' not in my_tuple) # True
---------------------
#tuple10.py
# Using a for loop to iterate through a tuple
for name in ('John', 'Kate'):
print("Hello", name)
#output:
# Hello John
# Hello Kate

Second Level Students 2021/2022 134


Data Structures and Algorithms with Python

tupe1=('a', 1, 2, 3, 'b', 5, 1, 2, 'a')


print(tupe1)
for name in tupe1:
print("Hello", name)
-------------------
Python supports a type of container like dictionaries called
“namedtuple()” present in module, “collections“. Like dictionaries
they contain keys that are hashed to a particular value. But on
contrary, it supports both access from key value and iteration, the
functionality that dictionaries lack.
#namedtuple1.py
# Python code to demonstrate namedtuple()

from collections import namedtuple

Student = namedtuple('Student',['name','age','DOB'])

# Adding values
S = Student('Nandini','19','2541997')
print("S=")
print(S)

# Access using index


print ("The Student age using index is : ",end ="")
print (S[1])

# Access using name


print ("The Student name using keyname is : ",end ="")
print (S.name)

Second Level Students 2021/2022 135


Data Structures and Algorithms with Python

5- deque

6- ChainMap

7- UserDict

8- UserList

9- UserString
This class also acts as a wrapper for the string elements. It also
used to manipulate the string values. Because normal string is not
allowing any manipulation like adding and removing. Here we are
having two classes UserString and MutableString. In this
MutableString not used most. Because its mostly in slicing process.
UserString is most used one for manipulation of String objects.

Questions:
1. Examples of linear collections are:
a. Sets and trees b. Lists and stacks
2. Examples of unordered collections are:
a. Queues and lists b. Sets and dictionaries
3. A hierarchical collection can represent a:
a. Line of customers at a bank b. File directory system
4. A graph collection best represents a:
a. Set of numbers
b. Map of flight paths between cities
5. In Python, a type conversion operation for two collections:
a. Creates copies of the objects in the source collection and adds
these new objects to a new instance of the destination collection

Second Level Students 2021/2022 136


Data Structures and Algorithms with Python

b. Adds references to the source collection’s objects to a new


instance of the destination collection
6. The == operation for two lists must:
a. Compare pairs of items at each position for equality
b. Merely verify that each item in one list is also in the other list
7. The == operation for two sets must:
a. Compare pairs of items at each position for equality
b. Verify that the sets are of the same size and that each item in one
set is also in the other set
8. The for loop on a list visits its items:
a. At each position, from the first one through the last one
b. In no particular order
9. The map function creates a sequence of the:
a. Items in a given collection that pass a Boolean test
b. Results of applying a function to the items in a given collection
10. The filter function creates a sequence of the:
a. Items in a given collection that pass a Boolean test
b. Results of applying a function to the items in a given collection

Second Level Students 2021/2022 137


Data Structures and Algorithms with Python

6. Stack and queue

A stack is a linear data structure that stores items in a Last-In/First-Out


(LIFO) or First-In/Last-Out (FILO) manner. In stack, a new element is
added at one end and an element is removed from that end only. The
insert and delete operations are often called push and pop.

The operations for putting items on and removing items from a stack
are called push and pop, respectively.
➢ Push: if (top==MAX), display Stack overflow else reading the
data and making stack [top] =data and incrementing the top
value by doing top ++.
➢ Pop: if (top==0 ), display Stack underflow else printing the
element at the top of the stack and top--.
➢ Display: IF (TOP==0), display Stack is empty else printing the
elements in the stack from stack [0] to stack [top].

Fig. 6-1 Some stack states

Second Level Students 2021/2022 138


Data Structures and Algorithms with Python

The functions associated with stack are:


• empty() – Returns whether the stack is empty – Time
Complexity : O(1)
• size() – Returns the size of the stack – Time Complexity :
O(1)
• top() – Returns a reference to the top most element of the
stack – Time Complexity : O(1)
• push(g) – Adds the element ‘g’ at the top of the stack –
Time Complexity : O(1)
• pop() – Deletes the top most element of the stack – Time
Complexity : O(1)

Applications of stacks in computer science are numerous, for


example:
1- Translating infix expressions to postfix form and evaluating postfix
expressions. The operator in an infix expression, like 3 + 4, appears
between its two operands, whereas the operator in a postfix
expression, such as 3 4 +, follows its two operands.
2- Programming languages and compilers:
- method calls are placed onto a stack (call=push, return=pop)
- compilers use stacks to evaluate expressions
3- Backtracking algorithms (occurring in problems such as
automated theorem proving and game playing).
‫الخوارزميات التراجعية تعمل علي تجريب كل الحلول الممكنة لحل مسألة (مشكلة) ما بحيث‬
‫تتميز تلك المسألة بعدم القدرة علي الحل إال بتجربة كل حل مرة واحدة وتزال الحلول التي‬
‫ مثال هدف لعبة سودوكو هو ملء كل‬.‫تفشل في تحقيق قيود المشكلة وتحقيق نتيجة مناسبة‬

Second Level Students 2021/2022 139


‫‪Data Structures and Algorithms with Python‬‬

‫المربعات باألرقام مع األخذ باالعتبار عدم تكرار أي رقم في أي صف أو عمود‪ .‬فيحاول‬


‫الالعب ملء كل المربعات رقم برقم وعندما يجد أن الرقم الحالي ال يمكن أن يؤدي إلي حل‬
‫يقوم بإزالته (يتراجع) عن الرقم ويجرب رقما اخر وهذه الطريقة أفضل من تجربة كل‬
‫المجموعات الممكنة من األرقام واحدة تلو األخري‪ .‬مثال آخر الستخدام الخوارزميات‬
‫التراجعية هو حل المتاهات اذ نجرب كل الطرق الممكنة للخروج من المتاهة فاذا وجدنا‬
‫الطريق مفتوحا نكمل في هذا المسار وإن وجدناه مسدودا نقوم بالرجوع خطوة إلي الوراء‬
‫ونجرب مسارا آخر وهكذا حتي نصل لنقطة الخروج‪.‬‬
‫يشير مصطلح التراجع الي أنه اذا لم يكن الحل الحالي مناسبا‪ ،‬فعليك التراجع وتجريب الحلول‬
‫األخري‪ ،‬ويستحدم في حل المشكالت التي لها حلول متعددة ‪ ،‬أما اذا أردنا حال مثاليا ‪-‬كايجاد‬
‫أقصر الطرق للخروج من المتاهة‪ -‬يجب علينا هنا استخدام خوارزميات البرمجة الديناميكية‪.‬‬
‫‪4- Managing computer memory in support of function and method‬‬
‫‪calls.‬‬
‫‪5- Supporting the undo feature in text editors, word processors,‬‬
‫‪spreadsheet programs, drawing programs, and similar applications.‬‬
‫‪6- Maintaining a history of the links visited by a web browser.‬‬
‫‪Internet Web browsers store the addresses of recently visited sites in‬‬
‫‪a stack. Each time a user visits a new site, that site’s address is‬‬
‫‪“pushed” onto the stack of addresses. The browser then allows the‬‬
‫”‪user to “pop” back to previously visited sites using the “back‬‬
‫‪button.‬‬

‫‪Undo stack‬‬

‫‪This adds a new item to the undo stack:‬‬

‫‪Second Level Students 2021/2022‬‬ ‫‪140‬‬


Data Structures and Algorithms with Python

You can see that the stack now has an Add Function operation on it.
After adding the function, you delete a word from a comment. This
also gets added to the undo stack:

Notice how the Delete Word item is placed on top of the stack. Finally,
you indent a comment so that it’s lined up properly:

Second Level Students 2021/2022 141


Data Structures and Algorithms with Python

Your editor undoes the indent, and the undo stack now contains two
items. This operation is the opposite of push and is commonly
called pop.
When you hit undo again, the next item is popped off the stack:

Second Level Students 2021/2022 142


Data Structures and Algorithms with Python

Stack Implementation
We will discuss the implementation of stack using data structures and
modules from Python library. Stack in Python can be implemented
using following ways:

• list
• collections.deque
• queue.LifoQueue

Stacks are usually implemented with arrays; queues are often


implemented using another structure called a linked list.

A stack type is not built into Python.


Python programmers can use a Python list to emulate an array-
based stack. If you view the end of a list as the top of a stack, the list
method append pushes an element onto this stack, whereas the list
method pop removes and returns the element at its top. The main
drawback of this option is that all the other list operations can
manipulate the stack as well. These include the insertion,
replacement, and removal of an element at any position. These extra
operations violate the spirit of a stack as an abstract data type.
In addition to the push and pop operations, a stack interface
provides an operation named peek for examining the element at the
top of a stack.

Second Level Students 2021/2022 143


Data Structures and Algorithms with Python

Like other collections, the stack can also include the clear, isEmpty,
len, str, in, and + operations, as well as an iterator.
These operations are listed as Python methods in Table 6-1, where
the variable s refers to a stack.
Note that the methods pop and peek have an important
precondition and raise an exception if the user of the stack does not
satisfy that precondition.
Stack method What it Does
s.isEmpty() Returns True if s is empty or False otherwise.
s.__len__() Same as len(s). Returns the number of items in s.
s.__str__() Same as str(s), Returns the string representation
of s.
s.__iter__() Same as iter(s) or for item in s: . Visits each item
in s from bottom to top.
s.__contains__(item) Same as item in s. Returns True if item is in s
or False otherwise.
s1.__add__(s2) Same as s1+s2. Returns a new stack
containing the items in s1 and s2.
s.__eq__(anyObject) Same a s==anyObject. Returns True if s
equals anyObject or False otherwise. Two
stacks are equal if the items at corresponding
positions are equal.
s.clear() Makes s become empty.
s.peek() Returns the item at the top of s. Precondition:
s must not be empty; raises a keyerror if the
stack is empty.
s.push(item) Adds item to the top of s.
s.pop() Removes and returns the item at the top of s.
Precondition: s must not be empty; raises a
KeyError if the stack is empty.
Table 6-1

Second Level Students 2021/2022 144


Data Structures and Algorithms with Python
# Python program to demonstrate stack implementation
# using list

stack = []

# append() function to push element in the stack

stack.append('a')
stack.append('b')
stack.append('c')

print('Initial stack')
print(stack)

# pop() fucntion to pop element from stack in LIFO order


print('\nElements poped from stack:')
print(stack.pop())
print(stack.pop())
print(stack.pop())

print('\nStack after elements are poped:')


print(stack)

# uncommenting print(stack.pop())
# will cause an IndexError as the stack is now empty

Output:

Initial stack
['a', 'b', 'c']

Elements poped from stack:


c
b
a

Stack after elements are poped:


[]

Second Level Students 2021/2022 145


Data Structures and Algorithms with Python

The list methods make it very easy to use a list as a stack, where the
last element added is the first element retrieved (“last-in, first-out”).
To add an item to the top of the stack, use append(). To retrieve an
item from the top of the stack, use pop() without an explicit index.
For example:

>>> stack.append(6)
>>> stack.append(7)
>>> stack
[3, 4, 5, 6, 7]
>>> stack.pop()
7
>>> stack
[3, 4, 5, 6]
>>> stack.pop()
6
>>> stack.pop()
5
>>> stack
[3, 4]

---------------------------------------
# Module: stack3.py
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):

Second Level Students 2021/2022 146


Data Structures and Algorithms with Python

return self.items[len(self.items)-1]

def size(self):
return len(self.items)
#Module: stack4.py
from stack3 import Stack

s=Stack()
print(s.isEmpty())
s.push(4)
s.push('dog')
print(s.peek())
s.push(True)
print(s.size())
print(s.isEmpty())
s.push(8.4)
print(s.pop())
print(s.pop())
print(s.size())

Second Level Students 2021/2022 147


Data Structures and Algorithms with Python

Table 6-2

Implement Stack with linked list:


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

def push_val(self, data):


if self.head is None:
self.head = Node(data)

else:
newNode = Node(data)

Second Level Students 2021/2022 148


Data Structures and Algorithms with Python

newNode.next = self.head
self.head = newNode

def pop_val(self):
if self.head is None:
return None
else:
del_Val = self.head.data
self.head = self.head.next
return del_Val

my_instance = Stack_linkedList()
while True:
print('push <value>')
print('pop')
print('quit')
my_input = input('What action would you like to perform ? ').split()

operation = my_input[0].strip().lower()
if operation == 'push':
my_instance.push_val(int(my_input[1]))
elif operation == 'pop':
del_Val = my_instance.pop_val()
if del_Val is None:
print('The stack is empty.')
else:
print('The deleted value is : ', int(del_Val))
elif operation == 'quit':
break

Second Level Students 2021/2022 149


Data Structures and Algorithms with Python

Linked List Implementation of Stack:


We can represent a stack as a linked list. In a stack push and pop
operations are performed at one end called top. We can perform
similar operations at one end of list using top pointer.

Source code for stack operations, using linked list

# Python program for linked list implementation of stack


# Class to represent a node
class StackNode:
# Constructor to initialize a node
def __init__(self, data):
self.data = data
self.next = None
class Stack:
# Constructor to initialize the root of linked list
def __init__(self):
self.root = None
def isEmpty(self):
return True if self.root is None else False
def push(self, data):
newNode = StackNode(data)
Second Level Students 2021/2022 150
Data Structures and Algorithms with Python

newNode.next = self.root
self.root = newNode
print ("%d pushed to stack" %(data))

def pop(self):
if (self.isEmpty()):
return float("-inf")
temp = self.root
self.root = self.root.next
popped = temp.data
return popped

def peek(self):
if self.isEmpty():
return float("-inf")
return self.root.data

# Program to test above class


stack = Stack()
stack.push(10)
stack.push(20)
stack.push(30)
print ("%d popped from stack" %(stack.pop()))
print ("Top element is %d " %(stack.peek()))

# Python program to demonstrate


# stack implementation using a linked list.
# node class
class Node:
def __init__(self, value):
self.value = value
self.next = None

class Stack:

# Initializing a stack.
# Use a dummy node, which is
# easier for handling edge cases.
def __init__(self):
self.head = Node("head")
self.size = 0

# String representation of the stack


def __str__(self):
Second Level Students 2021/2022 151
Data Structures and Algorithms with Python
cur = self.head.next
out = ""
while cur:
out += str(cur.value) + "->"
cur = cur.next
return out[:-3]

# Get the current size of the stack


def getSize(self):
return self.size

# Check if the stack is empty


def isEmpty(self):
return self.size == 0

# Get the top item of the stack


def peek(self):

# Sanitary check to see if we


# are peeking an empty stack.
if self.isEmpty():
raise Exception("Peeking from an empty stack")
return self.head.next.value

# Push a value into the stack.


def push(self, value):
node = Node(value)
node.next = self.head.next
self.head.next = node
self.size += 1

# Remove a value from the stack and return.


def pop(self):
if self.isEmpty():
raise Exception("Popping from an empty stack")
remove = self.head.next
self.head.next = self.head.next.next
self.size -= 1
return remove.value

# Driver Code
if __name__ == "__main__":
stack = Stack()
for i in range(1, 11):
stack.push(i)
print(f"Stack: {stack}")

for _ in range(1, 6):


remove = stack.pop()
print(f"Pop: {remove}")
print(f"Stack: {stack}")

Second Level Students 2021/2022 152


Data Structures and Algorithms with Python

Output:
Stack: 10 -> 9 -> 8 -> 7 -> 6 -> 5 -> 4 -> 3 -> 2 -> 1

Pop: 10
Pop: 9
Pop: 8
Pop: 7
Pop: 6

Stack: 5 -> 4 -> 3 -> 2 -> 1

Implementation using collections.deque:

Will be discussed after queue.

Another code:
# Python program for implementation of stack

# import maxsize from sys module


# Used to return -infinite when stack is empty from sys import maxsize
# Function to create a stack. It initializes size of stack as 0

def createStack():
stack = []
return stack

# Stack is empty when stack size is 0


def isEmpty(stack):
return len(stack) == 0

Second Level Students 2021/2022 153


Data Structures and Algorithms with Python

# Function to add an item to stack. It increases size by 1


def push(stack, item):
stack.append(item)
print("pushed to stack " + item)

# Function to remove an item from stack. It decreases size by 1


def pop(stack):
if (isEmpty(stack)):
print("stack empty")
return str(-maxsize -1) #return minus infinite

return stack.pop()
# Call
stack = createStack()
print("maximum size of array is",maxsize)
push(stack, str(10))
push(stack, str(20))
push(stack, str(30))
print(pop(stack) + " popped from stack")
print(pop(stack) + " popped from stack")
print(pop(stack) + " popped from stack")
print(pop(stack) + " popped from stack")
push(stack, str(10))
push(stack, str(20))
push(stack, str(30))
print(pop(stack) + " popped from stack")

Example Application: Matching Parentheses

Compilers need to determine if the bracketing symbols in


expressions are balanced correctly.
For example, every opening [ should be followed by a properly
positioned closing ] and every ( by a ).

Second Level Students 2021/2022 154


Data Structures and Algorithms with Python

Table 6-3 Balanced and unbalanced Brackets in Expressions

To check an expression, take the following steps:


1. Scan across the expression, pushing opening brackets onto a
stack.
2. On encountering a closing bracket, if the stack is empty or if the
item on the top of the stack is not an opening bracket of the same
type, you know the brackets do not balance, so you can quit the
process and signal that the expression is improperly formed.
3. Otherwise, pop the item off the top of the stack and continue
scanning the expression.
4. When you reach the end of the expression, the stack should be
empty, and if it is not, you know the brackets do not balance.
Here is a Python script that implements this strategy for the two
types of brackets mentioned.

Assume that the module linkedstack includes the class


LinkedStack.

# stackTestBalancParentheses.py
# Python3 code to Check for balanced parentheses in an expression
open_list = ["[","{","("]
close_list = ["]","}",")"]

# Function to check parentheses

Second Level Students 2021/2022 155


Data Structures and Algorithms with Python

def check(myStr):
stack = []
for i in myStr:
if i in open_list:
stack.append(i)
elif i in close_list:
pos = close_list.index(i)
if ((len(stack) > 0) and
(open_list[pos] == stack[len(stack)-1])):
stack.pop()
else:
return "Unbalanced"
if len(stack) == 0:
return "Balanced"
else:
return "Unbalanced"

# Main code
string = "{[]{()}}"
print(string,"-", check(string))
string = "[{}{})(]"
print(string,"-", check(string))

string = "((()"
print(string,"-",check(string))

Second Level Students 2021/2022 156


Data Structures and Algorithms with Python

Exercises

Execute the following operations:

Evaluating Arithmetic Expressions


In daily life, people evaluate simple arithmetic expressions that they
give little thought to the rules involved. So, you might be surprised by
the difficulty of writing an algorithm to evaluate arithmetic
expressions. It turns out that an indirect approach to the problem
works best. First, you transform an expression from its familiar infix
form to a postfix form, and then you evaluate the postfix form. In
the infix form, each operator is located between its operands,

Second Level Students 2021/2022 157


Data Structures and Algorithms with Python

whereas in the postfix form, an operator immediately follows its


operands. Table 6-4 gives several simple examples.

There are similarities and differences between the two forms. In


both, operands appear in the same order. However, the operators do
not. The infix form sometimes requires parentheses; the postfix form
never does. Infix evaluation involves rules of precedence; postfix
evaluation applies operators as soon as they are encountered. For
instance, consider the steps in evaluating the infix expression 34 + 22
* 2 and the equivalent postfix expression 34 22 2 * +.

Infix evaluation: 34 + 22 * 2 → 34 + 44 → 78
Postfix evaluation: 34 22 2 * + → 34 44 + → 78

Table 6-4 Some Infix and Postfix Expressions

Second Level Students 2021/2022 158


Data Structures and Algorithms with Python

Example:
Evaluate the postfix expression: 6 5 2 3 + 8 * + 3 + *

Second Level Students 2021/2022 159


Data Structures and Algorithms with Python

Table 6-5
Code:
#H:\H:\data structures\data structures Python 2020\codes vs code\postfixEvaluate2.py
class Evaluate:
def __init__(self):
self.array = []
self.size = -1
def isEmpty(self):
if self.array == []:
return True
else:
return False
def pop(self):
if not self.isEmpty():
Second Level Students 2021/2022 160
Data Structures and Algorithms with Python

return self.array.pop()
else:
return "empty"
def push(self, op):
self.array.append(op)
def Postfix(self, exp):
for i in exp:
if i.isdigit():
self.push(i)
else:
val1 = self.pop()
val2 = self.pop()
#self.push(str(eval(val2 + i + val1)))
result=self.cal(val2,val1,i)
self.push(result)
#return int(self.pop())
return (self.pop())
def cal(self,op2,op1,i):
if i == '*':
return int(op2)*int(op1)
elif i == '/':
return int(op2)/int(op1)
elif i == '+':
return int(op2)+int(op1)
elif i == '-':
return int(op2)-int(op1)
obj=Evaluate()
#exp=input('enter the postfix expression')
exp= "45*4+6"
value= obj.Postfix(exp)
print('the result of postfix expression',exp,'is',value)

exp = "12+3+4+5+"
value= obj.Postfix(exp)
print('the result of postfix expression',exp,'is',value)
Second Level Students 2021/2022 161
Data Structures and Algorithms with Python

exp = "12345*+*+"
value= obj.Postfix(exp)
print('the result of postfix expression',exp,'is',value)

exp = "45*4+6/"
value= obj.Postfix(exp)
print('the result of postfix expression',exp,'is',value)

Evaluating Postfix Expressions


Evaluating a postfix expression involves three steps:
1. Scan across the expression from left to right.
2. On encountering an operator, apply it to the two preceding
operands and replace all three by the result.
3. Continue scanning until you reach the expression’s end, at which
point only the expression’s value remains.
To express this procedure as a computer algorithm, you use a stack
of operands. In the algorithm, the term token refers to either an
operand or an operator:

Create a new stack


While there are more tokens in the expression
Get the next token
If the token is an operand
Push the operand onto the stack

Second Level Students 2021/2022 162


Data Structures and Algorithms with Python

Else if the token is an operator


Pop the top two operands from the stack
Apply the operator to the two operands just popped
Push the resulting value onto the stack
Return the value at the top of the stack

The time complexity of the algorithm is O(n), where n is the number


of tokens in the expression.

Table 8-4 Tracing the Evaluation of a Postfix Expression

Converting Infix to Postfix

1. Start with an empty postfix expression and an empty stack, which


will hold Operators and left parentheses.
2. Scan across the infix expression from left to right.

Second Level Students 2021/2022 163


Data Structures and Algorithms with Python

3. On encountering an operand, append it to the postfix expression.


4. On encountering a left parenthesis, push it onto the stack.
5. On encountering an operator, pop off the stack all operators that
have equal or higher precedence, append them to the postfix
expression, and then push the scanned operator onto the stack.
6. On encountering a right parenthesis, shift operators from the stack
to the postfix expression until meeting the matching left parenthesis,
which is discarded.
7. On encountering the end of the infix expression, transfer the
remaining operators from the stack to the postfix expression.

Table 6-5 Tracing the Conversion of an Infix Expression to a Postfix Expression

Second Level Students 2021/2022 164


Data Structures and Algorithms with Python

Table 6-6 Tracing the Conversion of an Infix Expression to a Postfix Expression

# Python program to convert infix expression to postfix


# Class to convert the expression
import string
class Conversion:
# Constructor to initialize the class variables
def __init__(self, capacity):
self.top = -1
self.capacity = capacity
# This array is used a stack
self.array = []
# Precedence setting
self.output = []
self.precedence = {'+':1, '-':1, '*':2, '/':2, '^':3}
# check if the stack is empty

Second Level Students 2021/2022 165


Data Structures and Algorithms with Python

def isEmpty(self):
return True if self.top == -1 else False
# Return the value of the top of the stack

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

# Pop the element from the stack


def pop(self):
if not self.isEmpty():
self.top -= 1
return self.array.pop()
else:
return "$"

# Push the element to the stack


def push(self, op):
self.top += 1
self.array.append(op)
# A utility function to check is the given character is operand
def isOperand(self, ch):
return ch.isalpha()

# Check if the precedence of operator is strictly


# less than top of stack or not

def notGreater(self, i):


try:
a = self.precedence[i]
b = self.precedence[self.peek()]
return True if a <= b else False
except KeyError:
return False

# The main function that converts given infix expression


# to postfix expression
def infixToPostfix(self, exp):
# Iterate over the expression for conversion
for i in exp:
# If the character is an operand,

Second Level Students 2021/2022 166


Data Structures and Algorithms with Python

# add it to output
if self.isOperand(i):
self.output.append(i)

# If the character is an '(', push it to stack


elif i == '(':
self.push(i)
# If the scanned character is an ')', pop and
# output from the stack until and '(' is found
elif i == ')':
while( (not self.isEmpty()) and self.peek() != '('):
a = self.pop()
self.output.append(a)
if (not self.isEmpty() and self.peek() != '('):
return -1
else:
self.pop()
# An operator is encountered
else:
while(not self.isEmpty() and self.notGreater(i)):
self.output.append(self.pop())
self.push(i)

# pop all the operator from the stack


while not self.isEmpty():
self.output.append(self.pop())

result= "".join(self.output)
print(result)

# Test program
exp = "a+b*(c^d-e)^(f+g*h)-i"
obj = Conversion(len(exp))
obj.infixToPostfix(exp)

#infixTopostfix.py
# program to convert an infix expression to a postfix expression

# To check if the input character is an operator or a '('


Second Level Students 2021/2022 167
Data Structures and Algorithms with Python

def isOperator(input):

switch = {
'+': 1,
'-': 1,
'*': 1,
'/': 1,
'%': 1,
'(': 1,
}

return switch.get(input, False)

# To check if the input character is an operand


# ord('a') returns the integer 97 Unicode
def isOperand(input):

if ((ord(input) >= 65 and ord(input) <= 90) or


(ord(input) >= 97 and ord(input) <= 122)):
return 1

return 0

# Function to return precedence value


# if operator is present in stack
def inPrec(input):

switch = {
'+': 2,
'-': 2,
'*': 4,
'/': 4,
'%': 4,
'(': 0,
}
Second Level Students 2021/2022 168
Data Structures and Algorithms with Python

return switch.get(input, 0)

# Function to return precedence value


# if operator is present outside stack.
def outPrec(input):

switch = {
'+': 1,
'-': 1,
'*': 3,
'/': 3,
'%': 3,
'(': 100,
}

return switch.get(input, 0)

# Function to convert infix to postfix


def inToPost(input):

i=0
s = []

# While input is not NULL iterate


while (len(input) != i):

# If character an operand
if (isOperand(input[i]) == 1):
print(input[i], end = "")

# If input is an operator, push


elif (isOperator(input[i]) == 1):
if (len(s) == 0 or
outPrec(input[i]) >
Second Level Students 2021/2022 169
Data Structures and Algorithms with Python

inPrec(s[-1])):
s.append(input[i])

else:
while(len(s) > 0 and
outPrec(input[i]) <
inPrec(s[-1])):
print(s[-1], end = "")
s.pop()

s.append(input[i])

# Condition for opening bracket


elif(input[i] == ')'):
while(s[-1] != '('):
print(s[-1], end = "")
s.pop()

# If opening bracket not present


if(len(s) == 0):
print('Wrong input')
exit(1)

# pop the opening bracket.


s.pop()

i += 1

# pop the remaining operators


while(len(s) > 0):
if(s[-1] == '('):
print('Wrong input')
exit(1)

print(s[-1], end = "")


Second Level Students 2021/2022 170
Data Structures and Algorithms with Python

s.pop()

# Main code
input = "a+b*c-(d/e+f*g*h)"

Exercise

1- Translate by hand the following infix expressions to postfix form:


a. 33 - 15 * 6
b. 11 * (6 + 2)
c. 17 + 3 - 5
d. 22 - 6 + 33 / 4
2- Perform a complexity analysis for a conversion of infix to postfix

Backtracking
A backtracking algorithm begins in a predefined starting state and
then moves from state to state in search of a desired ending state. At
any point along the way, when there is a choice between several
alternative states, the algorithm picks one, possibly at random, and
continues. If the algorithm reaches a state that represents an
undesirable outcome, it backs up to the last point at which there was
an unexplored alternative and tries it. In this way, the algorithm
either exhaustively searches all states, or it reaches the desired
ending state.

Second Level Students 2021/2022 171


Data Structures and Algorithms with Python

Queues
queue: Retrieves elements in the order they were added.
– First-In, First-Out ("FIFO")
– Elements are stored in order of insertion but don't have
indexes.
– Client can only add to the end of the queue, and can only
examine/remove the front of the queue.

Basic queue operations:


- add (enqueue): Add an element to the back.
- remove (dequeue): Remove the front element.

Queues Applications

• Operating systems:

– queue of print jobs to send to the printer

– queue of programs / processes to be run

– queue of network data packets to send

• Programming:

– modeling a line of customers or clients

Second Level Students 2021/2022 172


Data Structures and Algorithms with Python

– storing a queue of computations to be performed in


order

• Real world examples:

– people on an escalator or waiting in a line.

– cars at a gas station (or on an assembly line).

Representation of a Queue using Array:


Consider a queue, which can hold maximum of five elements. Initially
the queue is empty (Front=Rear=0).

Insert 11, then Rear= Rear+1=1, Front=0

Insert 22, then Rear= Rear+1=2 , Front=0

Second Level Students 2021/2022 173


Data Structures and Algorithms with Python

Insert 33, then Rear= Rear+1=3, Front=0

Delete an element, any delete will be in the front, then Rear= 3,


Front=Front+1=1

Delete another element, then Rear= 3, Front=Front+1=2

Insert 44 and 55, then Rear= 5, Front= 2

Can you add any new element now? No, even though there are two
free positions. To over come this problem the elements of the queue
are to be shifted towards the beginning of the queue so that it creates

Second Level Students 2021/2022 174


Data Structures and Algorithms with Python

vacant position at the rear end. Then the FRONT and REAR are to be
adjusted properly. The element 66 can be inserted at the rear end.

Rear = 4, Front =0

This difficulty can overcome if we treat queue as a circular queue.

Using Lists as Queues


It is possible to use a list as a queue, where the first element added is
the first element retrieved (“first-in, first-out”); however, lists are not
efficient for this purpose. While appends and pops from the end of
list are fast, doing inserts or pops from the beginning of a list is slow
(because all the other elements have to be shifted by one).
To implement a queue, use collections.deque which was designed to
have fast appends and pops from both ends.

Example:
#Queue_deque

from collections import deque


queue = deque(["Eric", "John", "Michael"])

print(queue)
queue.append("Terry") # Terry arrives

Second Level Students 2021/2022 175


Data Structures and Algorithms with Python

print('append')
print(queue)
queue.append("Graham") # Graham arrives
print('append')
print(queue)
queue.popleft() # The first to arrive
print('remove left "front"')
print(queue)
queue.popleft() # The second to arrive now leaves
print('remove left "front"')
print(queue) # Remaining queue in order of arrival)

Implement queue using 1-D array, perform creation,


deletion and display the elements in a queue.

front is always 1 less than the actual front of the queue and rear
always points to the last element in the queue. Thus, front = rear if
and only if there are no elements in the queue. The initial condition
then is front = rear = 0.

Implementing Stack using collections.deque:

Python has a deque (pronounced 'deck') library that provides a


sequence with efficient methods to work as a stack or a queue.

deque is short for Double Ended Queue - a generalized queue that can
get the first or last element that's stored:

# use deque to create queue/stack


# pop from last –stack
#delete from left --queue
from collections import deque

Second Level Students 2021/2022 176


Data Structures and Algorithms with Python

numbers=deque()
print(numbers)
numbers.append(99)
print(numbers)
numbers.append(15)
print(numbers)
numbers.append(82)
print(numbers)
numbers.append(50)
print(numbers)
numbers.append(47)
print(numbers)
last_item=numbers.pop()
print('delete the item:')
print(last_item) #47
print(numbers) #[99, 15, 82, 50]
print('delete the item:')
first_item= numbers.popleft()
print(first_item) #99
print('last numbers')
print(numbers) # [15, 82, 50]

Second Level Students 2021/2022 177


Data Structures and Algorithms with Python

Python stack can be implemented using deque class from collections


module. Deque is preferred over list in the cases where we need
quicker append and pop operations from both the ends of the
container, as deque provides an O(1) time complexity for append and
pop operations as compared to list which provides O(n) time
complexity.
The same methods on deque as seen in list are used, append() and
pop().

# Python program to demonstrate stack implementation


# using collections.deque

from collections import deque

stack = deque()

# append() function to push


# element in the stack
stack.append('a')
stack.append('b')
stack.append('c')

Second Level Students 2021/2022 178


Data Structures and Algorithms with Python

print('Initial stack:')
print(stack)

# pop() fucntion to pop


# element from stack in
# LIFO order
print('\nElements poped from stack:')
print(stack.pop())
print(stack.pop())
print(stack.pop())

print('\nStack after elements are poped:')


print(stack)

# uncommenting print(stack.pop())
# will cause an IndexError
# as the stack is now empty
Output:
Initial stack:
deque(['a', 'b', 'c'])

Elements poped from stack:


c
b
a

Stack after elements are poped:


deque([])

Stack Implementation using queue module

Queue module also has a LIFO Queue, which is basically a Stack. Data
is inserted into Queue using put() function and get() takes data out
from the Queue.

Second Level Students 2021/2022 179


Data Structures and Algorithms with Python

There are various functions available in this module:

• maxsize – Number of items allowed in the queue.


• empty() – Return True if the queue is empty, False otherwise.
• full() – Return True if there are maxsize items in the queue. If the
queue was initialized with maxsize=0 (the default), then full()
never returns True.
• get() – Remove and return an item from the queue. If queue is
empty, wait until an item is available.
• get_nowait() – Return an item if one is immediately available,
else raise QueueEmpty.
• put(item) – Put an item into the queue. If the queue is full, wait
until a free slot is available before adding the item.
• put_nowait(item) – Put an item into the queue without
blocking.
• qsize() – Return the number of items in the queue. If no free slot
is immediately available, raise QueueFull.

# Python program to demonstrate stack implementation


# using queue module

from queue import LifoQueue

# Initializing a stack
stack = LifoQueue(maxsize = 3)

# qsize() show the number of elements


# in the stack
print(stack.qsize())

# put() function to push


# element in the stack
stack.put('a')

Second Level Students 2021/2022 180


Data Structures and Algorithms with Python
stack.put('b')
stack.put('c')

print("Full: ", stack.full())


print("Size: ", stack.qsize())

# get() fucntion to pop


# element from stack in
# LIFO order
print('\nElements poped from the stack')
print(stack.get())
print(stack.get())
print(stack.get())

print("\nEmpty: ", stack.empty())


Output:
0
Full: True
Size: 3
Elements poped from the stack
c
b
a
Empty: True

Linked List Implementation of Queue


We can represent a queue as a linked list. In a queue data is deleted
from the front end and inserted at the rear end. We can perform
similar operations on the two ends of a list. We use two pointers
front and rear for our linked queue implementation.

Second Level Students 2021/2022 181


Data Structures and Algorithms with Python

Code:
front = 0
rear = 0
mymax = 3
# Function to create a stack. It initializes size of stack as 0
def createQueue():
queue = []
return queue

# Stack is empty when stack size is 0


def isEmpty(queue):
return len(queue) == 0

# Function to add an item to stack. It increases size by 1


def enqueue(queue,item):
queue.append(item)

# Function to remove an item from stack. It decreases size by 1

def dequeue(queue):
if (isEmpty(queue)):
return "Queue is empty"
item=queue[0]
del queue[0]
return item

# Test program

queue = createQueue()
while True:
print("1 Enqueue")
print("2 Dequeue")
print("3 Display")
print("4 Quit")

ch=int(input("Enter choice"))
if(ch==1):
if(rear < mymax):
item=input("enter item")
Second Level Students 2021/2022 182
Data Structures and Algorithms with Python

enqueue(queue, item)
rear = rear + 1
else:
print("Queue is full")
elif(ch==2):
print(dequeue(queue))
elif(ch==3):
print(queue)
else:
break

Disadvantages of Linear Queue


There are two problems associated with linear queue. They are:
➢ Time consuming: linear time to be spent in shifting the
elements to the beginning of the queue.
➢ Signaling queue full: even if the queue is having vacant
position.

DEQUE(Double Ended Queue)


A double-ended queue (dequeue, often abbreviated to deque,
pronounced deck) generalizes a queue, for which elements can be
added to or removed from either the front (head) or back (tail).It is
also often called a head-tail linked list. Like an ordinary queue, a
double-ended queue is a data structure it supports the following
operations: enq_front, enq_back, deq_front, deq_back, and empty.
Dequeue can be behave like a queue by using only enq_front and
deq_front , and behaves like a stack by using only enq_front and
deq_rear.
The DeQueue is represented as follows.

Second Level Students 2021/2022 183


Data Structures and Algorithms with Python

DEQUE can be represented in two ways they are


1) Input restricted DEQUE(IRD)
2) output restricted DEQUE(ORD)

The output restricted DEQUE allows deletions from only one end and
input restricted DEQUE allow insertions at only one end. The DEQUE
can be constructed in two ways they are
1) Using array
2)Using linked list

Operations in DEQUE
1. Insert element at back
2. Insert element at front
3. Remove element at front
4. Remove element at back

Insert_front
Is an operation used to push an element into the front of the Deque.

Second Level Students 2021/2022 184


Data Structures and Algorithms with Python

Insert_back
Is an operation used to push an element into the back of the Deque.

Remove_front
Is an operation used to pop an element into the front of the Deque.

Remove_back
Is an operation used to pop an element into the back of the Deque.

Second Level Students 2021/2022 185


Data Structures and Algorithms with Python

Applications of DEQUE
1. The A-Steal algorithm implements task scheduling for several
processors (multiprocessor scheduling).
2. The processor gets the first element from the deque.
3. When one of the processor completes execution of its own threads
it can steal a thread from another processor.
4. It gets the last element from the deque of another processor and
executes it.

Circular Queue:
Circular queue is a linear data structure. It follows FIFO principle. In
circular queue the last node is connected back to the first node to
make a circle.
• Circular linked list fallow the First In First Out principle
• Elements are added at the rear end and the elements are deleted
at front end of the queue
• Both the front and the rear pointers points to the beginning of the
array.
• It is also called as “Ring buffer”.
• Items can inserted and deleted from a queue in O(1) time.
Second Level Students 2021/2022 186
Data Structures and Algorithms with Python

Circular Queue can be created in three ways they are


1. Using single linked list
2. Using double linked list
3. Using arrays

Consider a circular queue which can hold maximum (MAX) of six


elements. Initially the queue is empty.

Queue Empty
MAX = 6
Front = Rear = 0
CO U NT = 0

Insert 11, then Front=0, Rear=(Rear+1)%6=1, Count=1

Insert 22, 33, 44, then Front=0, Rear=Rear%6=5, Count=5

Second Level Students 2021/2022 187


Data Structures and Algorithms with Python

Now, delete an element. The element deleted is the element at the


front of the circular queue. So, 11 is deleted.

Front= (Front + 1) % 6 = 1
Rear = 5
Count = Count - 1 = 4
Again, delete an element. The element to be deleted is always
pointed to by the FRONT pointer. So, 22 is deleted.

Front = (Front + 1) % 6 = 2

Second Level Students 2021/2022 188


Data Structures and Algorithms with Python
Rear = 5, CO U NT = CO U NT - 1 = 3

Front = 2, Rear = 2
Rear = Rear % 6 = 2
CO UNT = 6

Now, if we insert an element to the circular queue, as COUNT = MAX


we cannot add the element to circular queue. So, the circular queue
is full.

Exercises:

1) Write a method stutter that accepts a queue of integers as a


parameter and replaces every element of the queue with two
copies of that element.
– front [1, 2, 3] back
becomes
- front [1, 1, 2, 2, 3, 3] back

2) Write a method mirror that accepts a queue of strings as a


parameter and appends the queue's contents to itself in
reverse order.

Second Level Students 2021/2022 189


Data Structures and Algorithms with Python

– front [a, b, c] back


becomes
front [a, b, c, c, b, a] back
3) A postfix expression is a mathematical expression but with the
operators written after the operands rather than before.
1 + 1 becomes 1 1 +
1 + 2 * 3 + 4 becomes 1 2 3 * + 4 +
Write a method postfixEvaluate that accepts a postfix expression
string, evaluates it, and returns the result.
All operands are integers; legal operators are + and *
postFixEvaluate("1 2 3 * + 4 +") returns 11
 The algorithm: Use a stack
When you see operands, push them.
When you see an operator, pop the last two operands, apply the
operator, and push the result onto the stack.
When you're done, the one remaining stack element is the result.

Second Level Students 2021/2022 190


Data Structures and Algorithms with Python

7. Arrays and Linked Structures (lists)

Objectives:
✓ Create arrays
✓ Perform various operations on arrays
✓ Determine the running times and memory usage of array operations
✓ Describe how costs and benefits of array operations depend on how
arrays are represented in computer memory
✓ Create linked structures using singly linked nodes
✓ Perform various operations on linked structures with singly linked nodes

The Array Data Structure


An array represents a sequence of items that can be accessed or
replaced at given index positions. You are probably thinking that this
description resembles that of a Python list. In fact, the data structure
underlying a Python list is an array. Although Python programmers
would typically use a list where you might use an array, the array
rather than the list is the primary implementing structure in the
collections of Python and many other programming languages.
Some of what has been said about arrays also applies to Python lists,
but arrays are much more restrictive. A programmer can access and
replace an array’s items at given positions, examine an array’s length,
and obtain its string representation—but that’s all.

The programmer cannot add or remove positions or make the length


of the array larger or smaller. Typically, the length or capacity of an
array is fixed when it is created.

Second Level Students 2021/2022 191


Data Structures and Algorithms with Python

Common Questions on Arrays


• Find the second minimum element of an array
• First non-repeating integers in an array
• Merge two sorted arrays
• Rearrange positive and negative values in an array

Python’s array module does include an array class, which behaves


more like a list but is limited to storing numbers.
You need to define a new class named Array that adheres to the
restrictions mentioned earlier but can hold items of any type. This
Array class uses a Python list to hold its items.

The class defines methods that allow clients to use the subscript
operator [ ], the len function, the str function, and the for loop with
array objects. The Array methods needed for these operations are
listed in Table 7-1. The variable a in the left column refers to an Array
object.

User’s Array Operation Method in the Array Class


a=Array(10) _init_(capacity, filleValue=None)
len(a) _len_()
str(a) _str_()
for item in a: _iter_ ()
a[index] _getitem_(index)
a[index]=newitem _setitem_(index, newItem)

Table 7-1 Array Operations and the methods of the Array Class

Second Level Students 2021/2022 192


Data Structures and Algorithms with Python

Python automatically calls the Array object’s __iter__ method when


the Array object is traversed in a for loop. Note that the programmer
must specify the capacity or the physical size of an
array when it is created. The default fill value, None, can be
overridden to provide another fill value if desired.

Creating an Array
import array as arr
a = arr.array('d', [1.1, 3.5, 4.5])
print(a)

float type (letter d)

Second Level Students 2021/2022 193


Data Structures and Algorithms with Python

# array2.py
import array as arr
a = arr.array('i', [2, 4, 6, 8]) #'i' signed integer

print("First element:", a[0]) # 2


print("Second element:", a[1]) #4
print("Last element:", a[-1]) #8
Output

First element: 2
Second element: 4
Last element: 8

Slicing Python Arrays

We can access a range of items in an array by using the slicing


operator :.

# array3.py

import array as arr

numbers_list = [2, 5, 62, 5, 42, 52, 48, 5]

numbers_array = arr.array('i', numbers_list)

print(numbers_array[2:5]) # 3rd to 5th=62,5,42

print(numbers_array[:-5]) # beginning to 4th=

print(numbers_array[5:]) # 6th to end

print(numbers_array[:]) # beginning to end

Second Level Students 2021/2022 194


Data Structures and Algorithms with Python

Output:

array('i', [62, 5, 42])


array('i', [2, 5, 62])
array('i', [52, 48, 5])
array('i', [2, 5, 62, 5, 42, 52, 48, 5])

Changing and Adding Elements

Arrays are mutable; their elements can be changed in a similar way


as lists.

import array as arr

numbers = arr.array('i', [1, 2, 3, 5, 7, 10])

# changing first element

numbers[0] = 0

print(numbers) # Output: array('i', [0, 2, 3, 5, 7, 10])

# changing 3rd to 5th element

numbers[2:5] = arr.array('i', [4, 6, 8])

print(numbers) # Output: array('i', [0, 2, 4, 6, 8, 10])

Output

array('i', [0, 2, 3, 5, 7, 10])


array('i', [0, 2, 4, 6, 8, 10])

Second Level Students 2021/2022 195


Data Structures and Algorithms with Python

We can add one item to the array using the append() method, or add
several items using the extend() method.

import array as arr

numbers = arr.array('i', [1, 2, 3])

numbers.append(4)

print(numbers) # Output: array('i', [1, 2, 3, 4])

# extend() appends iterable to the end of the array

numbers.extend([5, 6, 7])

print(numbers) # Output: array('i', [1, 2, 3, 4, 5, 6, 7])

Output

array('i', [1, 2, 3, 4])


array('i', [1, 2, 3, 4, 5, 6, 7])

Here is the code for the Array class:

"""
An Array is like a list, but use only [], len, iter, and str.
To instantiate, use
Second Level Students 2021/2022 196
Data Structures and Algorithms with Python

<variable> = Array(<capacity>, <optional fill value>)


The fill value is None by default.
"""
class Array(object):
"""Represents an array."""

def __init__(self, capacity, fillValue = None):


"""Capacity is the static size of the array.
fillValue is placed at each position."""
self.items = list()
for count in range(capacity):
self.items.append(fillValue)
def __len__(self):
"""-> The capacity of the array."""
return len(self.items)
def __str__(self):
"""-> The string representation of the array."""
return str(self.items)
def __iter__(self):
"""Supports traversal with a for loop."""
return iter(self.items)
def __getitem__(self, index):
"""Subscript operator for access at index."""
return self.items[index]
def __setitem__(self, index, newItem):
"""Subscript operator for replacement at index."""
self.items[index] = newItem

Here is a code that shows the use of an array:


from arrays import Array
a = Array(5) # Create an array with 5 positions
len(a) # Show the number of positions #5
print(a) # Show the contents
#[None, None, None, None, None]
for i in range(len(a)): # Replace contents with 1..5
a[i] = i + 1
print(a[0]) # Access the first item

Second Level Students 2021/2022 197


Data Structures and Algorithms with Python

#1
for item in a: # Traverse the array to print all
print(item)
#1
#2
#3
#4
#5
As you can see, an array is a very restricted version of a list.

Random Access and Contiguous Memory

The subscript, or index operation, makes it easy for the programmer


to store or retrieve an item at a given position. Array indexing
operation is a random access very fast operation. During random
access, the computer obtains the location of the ith item by
performing a constant number of steps (no matter how large the
array), it takes the same amount of time to access the first item as it
does to access the last item. The computer supports random access
for arrays by allocating a block of contiguous Memory cells for the
array’s items.
As shown in Fig. 7-1. Each data item occupies a single memory cell,
although this need not be the case. Because the addresses of the
items are in numerical sequence, the address of an array item can be
computed by adding two values: the array’s base address (machine
address) and the item’s offset (array index).

Second Level Students 2021/2022 198


Data Structures and Algorithms with Python

Fig. 7-1 A block of contiguous memory

Arrays operations
We can also concatenate two arrays using + operator.

import array as arr

odd = arr.array('i', [1, 3, 5])


even = arr.array('i', [2, 4, 6])

numbers = arr.array('i') # creating empty array of integer


numbers = odd + even

print(numbers)

Output

array('i', [1, 3, 5, 2, 4, 6])

Removing Python Array Elements

We can delete one or more items from an array using


Python's del statement.

import array as arr

number = arr.array('i', [1, 2, 3, 3, 4])

del number[2] # removing third element


print(number) # Output: array('i', [1, 2, 3, 4])

Second Level Students 2021/2022 199


Data Structures and Algorithms with Python

del number # deleting entire array


print(number) # Error: array is not defined

Output

array('i', [1, 2, 3, 4])


Traceback (most recent call last):
File "<string>", line 9, in <module>
print(number) # Error: array is not defined
NameError: name 'number' is not defined

We can use the remove() method to remove the given item,


and pop() method to remove an item at the given index.

import array as arr

numbers = arr.array('i', [10, 11, 12, 12, 13])

numbers.remove(12)
print(numbers) # Output: array('i', [10, 11, 12, 13])

print(numbers.pop(2)) # Output: 12
print(numbers) # Output: array('i', [10, 11, 13])

Output

array('i', [10, 11, 12, 13])


12
array('i', [10, 11, 13])

Python Lists Vs Arrays

In Python, we can treat lists as arrays. However, we cannot constrain


the type of elements stored in a list. For example:

# elements of different types


Second Level Students 2021/2022 200
Data Structures and Algorithms with Python

a = [1, 3.5, "Hello"]

If you create arrays using the array module, all elements of the array
must be of the same numeric type.

import array as arr


# Error
a = arr.array('d', [1, 3.5, "Hello"])

Output

Traceback (most recent call last):


File "<string>", line 3, in <module>
a = arr.array('d', [1, 3.5, "Hello"])
TypeError: must be real number, not str

➢ Lists are much more flexible than arrays. They can store
elements of different data types including strings. And, if you
need to do mathematical computation on arrays and matrices,
you are much better off using something like NumPy.

index() :- This function returns the index of the first occurrence of


value mentioned in arguments.
reverse() :- This function reverses the array.

# index() and reverse()

import array
# initializing array with array values
# initializes array with signed integers
arr= array.array('i',[1, 2, 3, 1, 2, 5])

# printing original array


print ("The new created array is : ",end="")
for i in range (0,6):
print (arr[i],end=" ")

print ("\r")
Second Level Students 2021/2022 201
Data Structures and Algorithms with Python

# using index() to print index of 1st occurrenece of 2


print ("The index of 1st occurrence of 2 is : ",end="")
print (arr.index(2))

#using reverse() to reverse the array


arr.reverse()

# printing array after reversing


print ("The array after reversing is : ",end="")
for i in range (0,6):
print (arr[i],end=" ")

Output:
The new created array is : 1 2 3 1 2 5
The index of 1st occurrence of 2 is : 1
The array after reversing is : 5 2 1 3 2 1

Increasing Array size

Python’s list performs this operation during a call of the method


insert or append, when more memory for the array is needed. The
resizing process consists of three steps:

1. Create a new, larger array.


2. Copy the data from the old array to the new array.
3. Reset the old array variable to the new array object.

Here is the code for this operation:

if logicalSize == len(a):
temp = Array(len(a) + 1) # Create a new array
for i in range(logicalSize): # Copy data from the old
temp [i] = a[i] # array to the new array
a = temp # Reset the old array variable
# to the new array

Second Level Students 2021/2022 202


Data Structures and Algorithms with Python

When the array is resized, the number of copy operations is linear.


Thus, the overall time performance for adding n items to an array is 1
+ 2 + 3 + … + n or n (n+1)/2 or O(n2 ).

The process of decreasing the size of an array is the inverse of


increasing it. Here are the steps:
1. Create a new, smaller array.
2. Copy the data from the old array to the new array.
3. Reset the old array variable to the new array object.

if logicalSize <= len(a) // 4 and len(a) >= DEFAULT_CAPACITY * 2:


temp = Array(len(a) // 2) # Create new array
for i in range(logicalSize): # Copy data from old array
temp [i] = a [i] # to new array
a = temp # Reset old array variable to
# new array

Operation Running Time


ith
Access at position O(1), best and worst cases
Replacement at ith position O(1), best and worst cases
Insert at logical end O(1), average case
Insert at ith position O(n), average case
th
Remove from i position O(n), average case
Increase capacity O(n), best and worst cases
Decrease capacity O(n), best and worst cases
Remove from logical end O(1), average case
Insert at ith position O(n), average case
Remove from ith position O(n), average case
Table 7-2 The running times of Array Operations

Two-Dimensional Arrays (Grids)


The arrays studied so far can represent only simple sequences of
items and are called one-dimensional arrays.

Second Level Students 2021/2022 203


Data Structures and Algorithms with Python

Suppose this grid is named grid. To access an item in grid, you use
two subscripts to specify its row and column positions, remembering
that indexes start at 0:
x = grid[2][3] # Set x to 23, the value in (row 2, column 3)

Fig. 7-2 Two-dimensional array or grid with 4 rows and 5 columns

sum = 0
for row in range(grid.getHeight()): # Go through rows
for column in range(grid.getWidth()): # Go through columns
sum +=grid[row][column]

Creating and Initializing a Grid


Assume that there exists a Grid class for two-dimensional arrays. To
create a Grid object, you can run the Grid constructor with three
arguments: height, width, and an initial fill value

>>> from grid import Grid


>>> grid = Grid(4, 5, 0)
>>> print(grid)
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0

Example:

Second Level Students 2021/2022 204


Data Structures and Algorithms with Python

from arrays import Array


class Grid(object):
"""Represents a two-dimensional array."""
def __init__(self, rows, columns, fillValue = None):
self.data = Array(rows)
for row in range (rows):
self.data[row] = Array(columns, fillValue)
def getHeight(self):
"""Returns the number of rows."""
return len(self.data)
def getWidth(self):
"Returns the number of columns."""
return len(self.data[0])
def __getitem__(self, index):
"""Supports two-dimensional indexing
with [row][column]."""
return self.data[index]
def __str__(self):
"""Returns a string representation of the grid."""
result = ""
for row in range (self.getHeight()):
for col in range (self.getWidth()):
result += str(self.data[row][col]) + " "
result += "\n"
return result

Consider the following example of a 3x4 matrix implemented as a list


of 3 lists of length 4:

>>> matrix = [
... [1, 2, 3, 4],
... [5, 6, 7, 8],
... [9, 10, 11, 12],
... ]

To transpose rows and columns:

Second Level Students 2021/2022 205


Data Structures and Algorithms with Python

>>> [[row[i] for row in matrix] for i in range(4)]


[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
OR
>>> transposed = []
>>> for i in range(4):
transposed.append([row[i] for row in matrix])

>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
OR
>>> transposed = []
>>> for i in range(4):
# the following 3 lines implement the nested listcomp
transposed_row = []
for row in matrix:
transposed_row.append(row[i])
transposed.append(transposed_row)

>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

The zip() function would do a great job for this use case:

>>> list(zip(*matrix))
[(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]

del statement
There is a way to remove an item from a list given its index instead of
its value: the del statement. This differs from the pop() method
which returns a value. The del statement can also be used to remove
slices from a list or clear the entire list (which we did earlier by
assignment of an empty list to the slice). For example:

>>> a = [-1, 1, 66.25, 333, 333, 1234.5]


>>> del a[0]

Second Level Students 2021/2022 206


Data Structures and Algorithms with Python

>>> a
[1, 66.25, 333, 333, 1234.5]
>>> del a[2:4]
>>> a
[1, 66.25, 1234.5]
>>> del a[:]
>>> a
[]

>>> del a

Questions:
1- Write a code segment that searches a Grid object for a negative
integer. The loop should terminate at the first instance of a
negative integer in the grid, and the variables row and column
should be set to the position of that integer. Otherwise, the
variables row and column should equal the number of rows
and columns in the grid.
2- Describe the contents of the grid after you run the following
code segment:
matrix = Grid(3, 3)
for row in range(matrix.getHeight()):
for column in range(matrix.getWidth()):
matrix[row][column] = row * column

3- Write a code segment that initializes each cell in a two-


dimensional array with an integer that represents its two index
positions. Thus, if a position is (row, column), the integer value
at position (2, 3) is 23.

Second Level Students 2021/2022 207


Data Structures and Algorithms with Python

Tuples and Sequences


As lists and strings have many common properties, such as indexing and
slicing operations. They are two examples of sequence data types
(Sequence Types — list, tuple, range). Other sequence data types may be
added to Python. There is also another standard sequence data type:
the tuple.

A tuple consists of a number of values separated by commas, for instance:

>>> t = 12345, 54321, 'hello!'


>>> t[0]
12345
>>> t
(12345, 54321, 'hello!')
>>> # Tuples may be nested:
... u = t, (1, 2, 3, 4, 5)
>>> u
((12345, 54321, 'hello!'), (1, 2, 3, 4, 5))
>>> # Tuples are immutable:
... t[0] = 88888
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> # but they can contain mutable objects:
... v = ([1, 2, 3], [3, 2, 1])
>>> v
([1, 2, 3], [3, 2, 1])

As you see, on output tuples are always enclosed in parentheses, so


that nested tuples are interpreted correctly; they may be input with
or without surrounding parentheses, although often parentheses are
necessary anyway (if the tuple is part of a larger expression). It is not
possible to assign to the individual items of a tuple, however it is
possible to create tuples which contain mutable objects, such as lists.

Second Level Students 2021/2022 208


Data Structures and Algorithms with Python

Though tuples may seem similar to lists, they are often used in
different situations and for different purposes. Tuples are immutable,
and usually contain a heterogeneous sequence of elements that are
accessed via unpacking (see later in this section) or indexing (or even
by attribute in the case of namedtuples). Lists are mutable, and their
elements are usually homogeneous and are accessed by iterating
over the list.

A special problem is the construction of tuples containing 0 or 1


items: the syntax has some extra quirks to accommodate these.
Empty tuples are constructed by an empty pair of parentheses; a
tuple with one item is constructed by following a value with a comma
(it is not sufficient to enclose a single value in parentheses). Ugly, but
effective.

For example:

>>> empty = ()
>>> singleton = 'hello', # <-- note trailing comma
>>> len(empty)
0
>>> len(singleton)
1
>>> singleton
('hello',)

The statement t = 12345, 54321, 'hello!' is an example of tuple


packing: the values 12345, 54321 and 'hello!' are packed together in a
tuple. The reverse operation is also possible:

>>> x, y, z = t

This is called, appropriately enough, sequence unpacking and works for any
sequence on the right-hand side. Sequence unpacking requires that there
are as many variables on the left side of the equals sign as there are
Second Level Students 2021/2022 209
Data Structures and Algorithms with Python

elements in the sequence. Note that multiple assignment is really just a


combination of tuple packing and sequence unpacking.

Sets
Python also includes a data type for sets. A set is an unordered
collection with no duplicate elements. Basic uses include
membership testing and eliminating duplicate entries. Set objects
also support mathematical operations like union, intersection,
difference, and symmetric difference.

Curly braces or the set() function can be used to create sets. Note: to
create an empty set you have to use set(), not {}; the latter creates an
empty dictionary, a data structure that we discuss later.

>>> basket = {'apple', 'orange', 'apple', 'pear',


'orange', 'banana'}
>>> print(basket) # show that
duplicates have been removed
{'orange', 'banana', 'pear', 'apple'}
>>> 'orange' in basket # fast membership
testing
True
>>> 'crabgrass' in basket
False

>>> # Demonstrate set operations on unique letters from


two words
...
>>> a = set('abracadabra')
>>> b = set('alacazam')
>>> a # unique letters in
a
{'a', 'r', 'b', 'c', 'd'}
>>> a - b # letters in a but
not in b
{'r', 'd', 'b'}

Second Level Students 2021/2022 210


Data Structures and Algorithms with Python

>>> a | b # letters in a or b
or both
{'a', 'c', 'r', 'd', 'b', 'm', 'z', 'l'}
>>> a & b # letters in both a
and b
{'a', 'c'}
>>> a ^ b # letters in a or b
but not both
{'r', 'd', 'b', 'm', 'z', 'l'}

Similarly to list comprehensions, set comprehensions are also


supported:

>>> a = {x for x in 'abracadabra' if x not in 'abc'}


>>> a
{'r', 'd'}

Dictionaries
Another useful data type built into Python is the Dictionaries which
are sometimes found in other languages as “associative memories”
or “associative arrays”. Unlike sequences, which are indexed by a
range of numbers, dictionaries are indexed by keys, which can be any
immutable type; strings and numbers can always be keys. Tuples can
be used as keys if they contain only strings, numbers, or tuples; if a
tuple contains any mutable object either directly or indirectly, it
cannot be used as a key. You can’t use lists as keys, since lists can be
modified in place using index assignments, slice assignments, or
methods like append() and extend().

It is best to think of a dictionary as a set of key: value pairs, with the


requirement that the keys are unique (within one dictionary). A pair
of braces creates an empty dictionary: {}. Placing a comma-separated
list of key: value pairs within the braces adds initial key: value pairs to
the dictionary; this is also the way dictionaries are written on output.

Second Level Students 2021/2022 211


Data Structures and Algorithms with Python

The main operations on a dictionary are storing a value with some


key and extracting the value given the key. It is also possible to delete
a key:value pair with del. If you store using a key that is already in
use, the old value associated with that key is forgotten. It is an error
to extract a value using a non-existent key.

Performing list(d) on a dictionary returns a list of all the keys used in


the dictionary, in insertion order (if you want it sorted, just
use sorted(d) instead). To check whether a single key is in the
dictionary, use the in keyword.

Here is a small example using a dictionary:

>>> tel = {'jack': 4098, 'sape': 4139}


>>> tel['guido'] = 4127
>>> tel
{'jack': 4098, 'sape': 4139, 'guido': 4127}
>>> tel['jack']
4098
>>> del tel['sape']
>>> tel['irv'] = 4127
>>> tel
{'jack': 4098, 'guido': 4127, 'irv': 4127}
>>> list(tel)
['jack', 'guido', 'irv']
>>> sorted(tel)
['guido', 'irv', 'jack']
>>> 'guido' in tel
True
>>> 'jack' not in tel
False

The dict() constructor builds dictionaries directly from sequences of key-


value pairs:

>>> dict([('sape', 4139), ('guido', 4127), ('jack',


4098)])
{'sape': 4139, 'guido': 4127, 'jack': 4098}

Second Level Students 2021/2022 212


Data Structures and Algorithms with Python

In addition, dict comprehensions can be used to create dictionaries


from arbitrary key and value expressions:

>>> {x: x**2 for x in (2, 4, 6)}


{2: 4, 4: 16, 6: 36}

When the keys are simple strings, it is sometimes easier to specify


pairs using keyword arguments:

>>> dict(sape=4139, guido=4127, jack=4098)


{'sape': 4139, 'guido': 4127, 'jack': 4098}

Looping Techniques
When looping through dictionaries, the key and corresponding value
can be retrieved at the same time using the items() method.

>>> knights = {'gallahad': 'the pure', 'robin': 'the brave'}


>>> for k, v in knights.items():
... print(k, v)
...
gallahad the pure
robin the brave

When looping through a sequence, the position index and


corresponding value can be retrieved at the same time using
the enumerate() function.

>>> for i, v in enumerate(['tic', 'tac', 'toe']):


... print(i, v)
...
0 tic
1 tac
2 toe

To loop over two or more sequences at the same time, the entries can be
paired with the zip() function.

Second Level Students 2021/2022 213


Data Structures and Algorithms with Python

>>> questions = ['name', 'quest', 'favorite color']


>>> answers = ['lancelot', 'the holy grail', 'blue']
>>> for q, a in zip(questions, answers):
... print('What is your {0}? It is {1}.'.format(q,
a))
...
What is your name? It is lancelot.
What is your quest? It is the holy grail.
What is your favorite color? It is blue.

To loop over a sequence in reverse, first specify the sequence in a


forward direction and then call the reversed() function.

>>> for i in reversed(range(1, 10, 2)):


... print(i)
...
9
7
5
3
1

To loop over a sequence in sorted order, use the sorted() function


which returns a new sorted list while leaving the source unaltered.

>>> basket = ['apple', 'orange', 'apple', 'pear',


'orange', 'banana']
>>> for i in sorted(basket):
... print(i)
...
apple
apple
banana
orange
orange
pear

Second Level Students 2021/2022 214


Data Structures and Algorithms with Python

Using set() on a sequence eliminates duplicate elements. The use


of sorted() in combination with set() over a sequence is an idiomatic
way to loop over unique elements of the sequence in sorted order.

>>> basket = ['apple', 'orange', 'apple', 'pear',


'orange', 'banana']
>>> for f in sorted(set(basket)):
... print(f)
...
apple
banana
orange
pear

It is sometimes tempting to change a list while you are looping over


it; however, it is often simpler and safer to create a new list instead.

>>> import math


>>> raw_data = [56.2, float('NaN'), 51.7, 55.3, 52.5, float('NaN'), 47.8]
>>> filtered_data = []
>>> for value in raw_data:
... if not math.isnan(value):
... filtered_data.append(value)
...
>>> filtered_data
[56.2, 51.7, 55.3, 52.5, 47.8]

More on Conditions
The conditions used in while and if statements can contain any
operators, not just comparisons.

The comparison operators in and not in check whether a value


occurs (does not occur) in a sequence. The
operators is and is not compare whether two objects are really the

Second Level Students 2021/2022 215


Data Structures and Algorithms with Python

same object; this only matters for mutable objects like lists. All
comparison operators have the same priority, which is lower than
that of all numerical operators.

Comparisons can be chained. For example, a < b == c tests


whether a is less than b and moreover b equals c.

Comparisons may be combined using the Boolean


operators and and or, and the outcome of a comparison (or of any
other Boolean expression) may be negated with not. These have
lower priorities than comparison operators; between them, not has
the highest priority and or the lowest, so that A and not B or C is
equivalent to (A and (not B)) or C. As always, parentheses can be used
to express the desired composition.

The Boolean operators and and or are so-called short-


circuit operators: their arguments are evaluated from left to right,
and evaluation stops as soon as the outcome is determined. For
example, if A and C are true but B is false, A and B and C does not
evaluate the expression C. When used as a general value and not as a
Boolean, the return value of a short-circuit operator is the last
evaluated argument.

It is possible to assign the result of a comparison or other Boolean


expression to a variable. For example,

>>> string1, string2, string3 = '', 'Trondheim', 'Hammer


Dance'
>>> non_null = string1 or string2 or string3
>>> non_null
'Trondheim'

Note that in Python, unlike C, assignment inside expressions must be


done explicitly with the walrus operator :=. This avoids a common
class of problems encountered in C programs: typing = in an
expression when == was intended.

Second Level Students 2021/2022 216


Data Structures and Algorithms with Python

Comparing Sequences and Other Types


Sequence objects typically may be compared to other objects with
the same sequence type. The comparison
uses lexicographical ordering: first the first two items are compared,
and if they differ this determines the outcome of the comparison; if
they are equal, the next two items are compared, and so on, until
either sequence is exhausted. If two items to be compared are
themselves sequences of the same type, the lexicographical
comparison is carried out recursively. If all items of two sequences
compare equal, the sequences are considered equal. If one sequence
is an initial sub-sequence of the other, the shorter sequence is the
smaller (lesser) one. Lexicographical ordering for strings uses the
Unicode code point number to order individual characters. Some
examples of comparisons between sequences of the same type:

(1, 2, 3) < (1, 2, 4)


[1, 2, 3] < [1, 2, 4]
'ABC' < 'C' < 'Pascal' < 'Python'
(1, 2, 3, 4) < (1, 2, 4)
(1, 2) < (1, 2, -1)
(1, 2, 3) == (1.0, 2.0, 3.0)
(1, 2, ('aa', 'ab')) < (1, 2, ('abc', 'a'), 4)

Note that comparing objects of different types with < or > is legal
provided that the objects have appropriate comparison methods. For
example, mixed numeric types are compared according to their
numeric value, so 0 equals 0.0, etc. Otherwise, rather than providing
an arbitrary ordering, the interpreter will raise a TypeError exception.

Linked Structures (Linked Lists)


Linked structures are probably the most commonly used data
structures in programs. Like an array, a linked structure is a concrete
data type that implements many types of collections, including lists.
Second Level Students 2021/2022 217
Data Structures and Algorithms with Python

The disadvantages of arrays are:


• The size of the array is fixed. Most often this size is specified at
compile time. This makes the programmers to allocate arrays,
which seems "large enough" than required.
• Inserting new elements at the front is potentially expensive
because existing elements need to be shifted over to make room.
• Deleting an element from an array is not possible. Linked lists
have their own strengths and weaknesses, but they happen to be
strong where arrays are weak.
• Generally array's allocates the memory for all its elements in one
block whereas linked lists use an entirely different strategy.
Linked lists allocate memory for each element separately and only
when necessary.

Advantages of linked lists:


Linked lists have many advantages. Some of the very important
advantages are:
1. Linked lists are dynamic data structures. i.e., they can grow or
shrink during the execution of a program.
2. Linked lists have efficient memory utilization. Here, memory is not
preallocated. Memory is allocated whenever it is required and it is de-
allocated (removed) when it is no longer needed.
3. Insertion and Deletions are easier and efficient. Linked lists provide
flexibility in inserting a data item at a specified position and deletion
of the data item from the given position.
4. Many complex applications can be easily carried out with linked
lists.

Disadvantages of linked lists:


1. It consumes more space because every node requires a additional
pointer to store address of the next node.

Second Level Students 2021/2022 218


Data Structures and Algorithms with Python

2. Searching a particular element in list is difficult and also time


consuming.

Types of Linked Lists

1. Single Linked List.


2. Double Linked List.
3. Circular Linked List.
4. Circular Double Linked List.

Singly Linked Structures and Doubly Linked Structures

As the name implies, a linked structure consists of items that are


linked to other items.
Although many links among items are possible, the two simplest
linked structures are the singly linked structure and the doubly
linked structure.
It is useful to draw diagrams of linked structures using a box and
pointer notation.
Figure 7-2 uses this notation to show examples of the two kinds of
linked structures.

Fig. 7-2 Two types of linked lists

A user of a singly linked structure accesses the first item by following


a single external head link. The user then accesses other items by

Second Level Students 2021/2022 219


Data Structures and Algorithms with Python

chaining through the single links (represented by arrows in the


figure) that emanate from the items. Thus, in a singly linked
structure, it is easy to get to the successor of an item, but not so easy
to get to the predecessor of an item.
A doubly linked structure contains links running in both directions.
Thus, it is easy for the user to move to an item’s successor or to its
predecessor. A second external link, called the tail link, allows the
user of a doubly linked structure to access the last item directly.

The last item in either type of linked structure has no link to the next
item. The figure indicates the absence of a link, called an empty link,
by means of a slash instead of an arrow. Note also that the first item
in a doubly linked structure has no link to the preceding item.
Like arrays, these linked structures represent linear sequences of
items. However, the programmer who uses a linked structure cannot
immediately access an item by specifying its index position. Instead,
the programmer must start at one end of the structure and follow the
links until the desired position (or item) is reached. This property of
linked structures has important consequences for several operations,
as discussed shortly.

Noncontiguous Memory and Nodes


Recall that array items must be stored in contiguous memory. This
means that the logical sequence of items in the array is tightly
coupled to a physical sequence of cells in memory.
By contrast, a linked structure decouples the logical sequence of
items in the structure from any ordering in memory. That is, the cell
for a given item in a linked structure can be found anywhere in
memory as long as the computer can follow a link to its address or
location.
Second Level Students 2021/2022 220
Data Structures and Algorithms with Python

This kind of memory representation scheme is called noncontiguous


memory.
The basic unit of representation in a linked structure is a node. A
singly linked node contains the following components or fields:
•• A data item
•• A link to the next node in the structure
In addition to these components, a doubly linked node contains a
link to the previous node in the structure.

Figure 7-3 shows a singly linked node and a doubly linked node
whose internal links are empty.

Fig 7-3 Two types of nodes with empty links

Fig. 7-4 An arryay representation of a linked list

A circular linked list is one, which has no beginning and no end.


A circular double linked list is one in which has both the successor
and predecessor pointer in the circular manner.

Second Level Students 2021/2022 221


Data Structures and Algorithms with Python

Fig. 7-5 A circular double linked list

Defining a Singly Linked Node Class


Node classes are simple. Flexibility and ease of use are critical, so the
instance variables of a node object are usually referenced without
method calls, and constructors allow the user to set a node’s link(s)
when the node is created. As mentioned earlier, a singly linked node
contains just a data item and a reference to the next node. Here is the
code for a simple, singly linked node class:

class Node(object):
"""Represents a singly linked node."""
def __init__(self, data, next = None):
"""Instantiates a Node with a default next of None."""
self.data = data
self.next = next

Using the Singly Linked Node Class


Node variables are initialized to either the None value or a new Node
object. The next code segment shows some variations on these two
options:

# Just an empty link


node1 = None
# A node containing data and an empty link
node2 = Node("A", None)
# A node containing data and a link to node2
Second Level Students 2021/2022 222
Data Structures and Algorithms with Python

node3 = Node("B", node2)

Figure 7-6 shows the state of the three variables after this code is
run.

Fig. 7-6 Three external links

Note the following:


•• node1 points to no node object yet (is None).
•• node2 and node3 point to objects that are linked.
•• node2 points to an object whose next pointer is None.

node1 = Node("C", node3)


or
node1 = Node("C", None)
node1.next = node3

Use the Node class to create a singly linked structure and print its
contents:

from node import Node


head = None
# Add five nodes to the beginning of the linked structure
for count in range(1, 6):
head = Node(count, head)
# Print the contents of the structure
while head != None:
print(head.data)
head = head.next

Second Level Students 2021/2022 223


Data Structures and Algorithms with Python

Exercise

Write a code segment that transfers items from a full array to a singly
linked structure. The operation should preserve the ordering of the
items.

Operations on Singly Linked Structures

Traversal

Many applications simply need to visit each node without deleting it.
This operation, called a traversal, uses a temporary pointer variable
named probe.

probe = head
while probe != None:
<use or modify probe.data>
probe = probe.next

Second Level Students 2021/2022 224


Data Structures and Algorithms with Python

Fig. 7-7 Traversing a linked list

Searching
The sequential search of a linked structure resembles a traversal in
that you must start at the first node and follow the links until you
reach a sentinel. However, in this case, there are two possible
sentinels:
•• The empty link, indicating that there are no more data items to
examine
•• A data item that equals the target item, indicating a successful
search
Here is the form of the search for a given item:

probe = head

Second Level Students 2021/2022 225


Data Structures and Algorithms with Python

while probe != None and targetItem != probe.data:


probe = probe.next
if probe != None:
<targetItem has been found >
else:
<targetItem is not in the linked structure>

You can assume that 0 <= i< n, where n is the number of nodes in
the linked structure. Here is the form for accessing the ith item:

# Assumes 0 <= index < n


probe = head
while index > 0:
probe = probe.next
index -= 1
return probe.data

# linkedList6.py

# Python program to search an element in linked list

# Node class
class Node:

# Function to initialise the node object


def __init__(self, data):
self.data = data # Assign data
self.next = None # Initialize next as null

# Linked List class


class LinkedList:

Second Level Students 2021/2022 226


Data Structures and Algorithms with Python

def __init__(self):
self.head = None # Initialize head as None

# This function insert a new node at the


# beginning of the linked list
def push(self, new_data):

# Create a new Node


new_node = Node(new_data)

# 3. Make next of new Node as head


new_node.next = self.head

# 4. Move the head to point to new Node


self.head = new_node

def search(self, x):

# Initialize current to head


current = self.head

# loop till current not equal to None


while current != None:
if current.data == x:
return True # data found

current = current.next

return False # Data Not found


# Code execution starts here
if __name__ == '__main__':

# Start with the empty list


llist = LinkedList()

''' Use push() to construct below list


14->21->11->30->10 '''
llist.push(10);
llist.push(30);
llist.push(11);
llist.push(21);
llist.push(14);

Second Level Students 2021/2022 227


Data Structures and Algorithms with Python

if llist.search(21):
print("Yes")
else:
print("No")

# linkedList7.py
# Recursive Python program to search an element in linked list

# Node class
class Node:

# Function to initialise
# the node object
def __init__(self, data):
self.data = data # Assign data
self.next = None # Initialize next as null

class LinkedList:

def __init__(self):
self.head = None # Initialize head as None

# This function insert a new node at the beginning of the linked l


ist
def push(self, new_data):

# Create a new Node


new_node = Node(new_data)

# Make next of new Node as head


new_node.next = self.head

# Move the head to


# point to new Node
self.head = new_node
# Checks whether the value key
# is present in linked list
def search(self, li, key):

# Base case
if(not li):

Second Level Students 2021/2022 228


Data Structures and Algorithms with Python

return False

# If key is present in
# current node, return true
if(li.data == key):
return True

# Recur for remaining list


return self.search(li.next, key)
# Driver Code
if __name__=='__main__':

li = LinkedList()

li.push(1)
li.push(2)
li.push(3)
li.push(4)

key = 4

if li.search(li.head,key):
print("Yes")
else:
print("No")

Replacement
The replacement operations in a singly linked structure also employ
the traversal pattern.
In these cases, you search for a given item or a given position in the
linked structure and replace the item with a new item. The first
operation, replacing a given item, need not assume that the target
item is in the linked structure.
If the target item is not present, no replacement occurs and the
operation returns False. If the target is present, the new item

Second Level Students 2021/2022 229


Data Structures and Algorithms with Python

replaces it and the operation returns True. Here is the form of the
operation:

probe = head
while probe != None and targetItem != probe.data:
probe = probe.next
if probe != None:
probe.data = newItem
return True
else:
return False

The operation to replace the ith item assumes that 0 <= i <n.

# Assumes 0 <= index < n


probe = head
while index > 0:
probe = probe.next
index -= 1
probe.data = newItem

Both replacement operations are linear on the average.

Replace nodes with duplicates in linked list


Given a linked list that contains some random integers from 1 to n with
many duplicates. Replace each duplicate element that is present in
the linked list with the values n+1, n+2, n+3 and so on

Inserting at the Beginning

Second Level Students 2021/2022 230


Data Structures and Algorithms with Python

Some operations make the linked list more preferrable than arrays.

head = Node(newItem, head)

Two cases: 1- head is None 2- not empty

Fig. 7-8 Two cases of inserting an item at the beginning of a linked list

Inserting data at the beginning of a linked structure uses constant


time and memory, unlike the same operation with arrays.

Inserting at the End


Inserting an item at the end of an array (used in the append
operation of a Python list) requires constant time and memory,
unless the array must be resized. The same process for a singly linked
structure must consider two cases:
•• The head pointer is None, so the head pointer is set to the new
node.
•• The head pointer is not None, so the code searches for the last
node and aims its next pointer at the new node.

newNode = Node(newItem)

Second Level Students 2021/2022 231


Data Structures and Algorithms with Python

if head is None:
head = newNode
else:
probe = head
while probe.next != None:
probe = probe.next
probe.next = newNode

Fig. 7-9 Inserting an item at the end of a linked list

Removing at the Beginning

Assume that there is at least one node in the structure. The operation
returns the removed item.

Here is the form:

# Assumes at least one node in the structure


removedItem = head.data

Second Level Students 2021/2022 232


Data Structures and Algorithms with Python

head = head.next
return removedItem

Fig. 7-10 Removing an item at the beginning of a linked list

The operation uses constant time and memory, unlike the same
operation for arrays.

Removing at the End


Removing an item at the end of an array (used in the Python list
method pop) requires constant time and memory, unless you must
resize the array. The same process for a singly linked structure
assumes at least one node in the structure. There are then two cases
to consider:
•• There is just one node. The head pointer is set to None.
•• There is a node before the last node. The code searches for this
second-to-last node and sets its next pointer to None.

In either case, the code returns the data item contained in the
deleted node. Here is the form:

# Assumes at least one node in structure


removedItem = head.data

Second Level Students 2021/2022 233


Data Structures and Algorithms with Python

if head.next is None:
head = None
else:
probe = head
while probe.next.next != None:
probe = probe.next
removedItem = probe.next.data
probe.next = None
return removedItem

Fig. 7-11 Removing an item at the end of a linked list

This operation is linear in time and constant in memory.

Inserting at Any Position

Second Level Students 2021/2022 234


Data Structures and Algorithms with Python

The insertion of an item at the ith position in an array requires


shifting items from position i down to position n – 1. Thus, you
actually insert the item before the item currently at position i so that
the new item occupies position i and the old item occupies position i
+ 1.
What about the cases of an empty array or an index that is greater
than n – 1?
If the array is empty, the new item goes at the beginning, whereas if
the index is greater than or equal to n, the item goes at the end.

The insertion of an item at the ith position in a linked structure must


deal with the same cases.
• insertion at the beginning.
• insertion at some other position i, the operation must first find
the node at position i – 1 (if i <n) or the node at position n – 1 (if
i >= n). Then there are two cases to consider:
•• That node’s next pointer is None. This means that i >= n, so you
should place the new item at the end of the linked structure.
•• That node’s next pointer is not None. That means that 0 < i < n, so
you must place the new item between the node at position i – 1 and
the node at position i.

if head is None or index <= 0:


head = Node(newItem, head)
else:
# Search for node at position index - 1 or the last position
probe = head
while index > 1 and probe.next != None:
probe = probe.next
index -= 1
# Insert new node after node at position index - 1
# or last position
probe.next = Node(newItem, probe.next)

Second Level Students 2021/2022 235


Data Structures and Algorithms with Python

Fig. 7-12 Inserting an item between two items in a linked list

Exercise: Insert an item before a given item.

Removing at Any Position


The removal of the ith item from a linked structure has three cases:

•• i <= 0—You use the code to remove the first item.


•• 0 <i < n—You search for the node at position i – 1, as in insertion,
and remove the Following node.
•• i >= n—You remove the last node.
Assume that the linked structure has at least one item.
However, you must allow the probe pointer to go no farther than the
second node from the end of the structure.

Here is the code:

Second Level Students 2021/2022 236


Data Structures and Algorithms with Python

# Assumes that the linked structure has at least one item


if index <= 0 or head.next is None:
removedItem = head.data
head = head.next
return removedItem
else:
# Search for node at position index - 1 or
# the next to last position
probe = head
while index > 1 and probe.next.next != None:
probe = probe.next
index -= 1
removedItem = probe.next.data
probe.next = probe.next.next
return removedItem

Second Level Students 2021/2022 237


Data Structures and Algorithms with Python

Fig. 7-12 Removing an item between two items in a linked list

Second Level Students 2021/2022 238


Data Structures and Algorithms with Python

Complexity Trade-Off: Time, Space, and Singly Linked Structures

Operation Running time


Access at ith position O(n), average case
Replacement at ith position O(n), average case
Insert at beginning O(1), best and worst case
Remove from beginning O(1), best and worst case
Insert at ith position O(n), average case
Remove from ith position O(n), average case
Table 7-2 Complexity of singly linked list

The main advantage of the singly linked structure over the array is
not time performance but memory performance. Resizing an array,
when this must occur, is linear in time and memory. Resizing a linked
structure, which occurs upon each insertion or removal,
is constant in time and memory. Moreover, no memory ever goes to
waste in a linked structure. The physical size of the structure never
exceeds the logical size. Linked structures
do have an extra memory cost in that a singly linked structure must
use n cells of memory for the pointers. This cost increases for doubly
linked structures, whose nodes have two links.

Questions:
Assume that the position of an item to be removed from a singly
linked structure has been located. State the run-time complexity for
completing the removal operation from that point.

A Circular Linked Structure with a Dummy Header Node


The insertion and the removal of the first node are special cases of
the insert ith and remove ith operations on singly linked structures.
These cases are special because the head pointer must be reset. You
can simplify these operations by using a circular linked structure
with a dummy header node. A circular linked structure contains a
Second Level Students 2021/2022 239
Data Structures and Algorithms with Python

link from the last node back to the first node in the structure. There is
always at least one node in this implementation. This node, the
dummy header node, contains no data but serves as a marker for the
beginning and the end of the linked structure.

Initially, in an empty linked structure, the head variable points to the


dummy header node, and the dummy header node’s next pointer
points back to the dummy header node itself, as shown in Figure 7-
13.

Fig. 7-13 An empty circular linked list

The first node to contain data is located after the dummy header
node. This node’s next pointer then points back to the dummy
header node in a circular fashion, as shown in Figure 7-14.

Fig. 7-14 A circular linked list after inserting the first node

The search for the ith node begins with the node after the dummy
header node. Assume that the empty linked structure is initialized as
follows:

head = Node(None, None)


head.next = head

Here is the code for insertions at the ith position using this new
representation of a linked structure:

Second Level Students 2021/2022 240


Data Structures and Algorithms with Python

# Search for node at position index - 1 or the last position


probe = head
while index > 0 and probe.next != head:
probe = probe.next
index -= 1
# Insert new node after node at position index - 1 or
# last position
probe.next = Node(newItem, probe.next)

The advantage of this implementation is that the insertion and


removal operations have only one case to consider—the case in
which the ith node lies between a prior node and the current ith
node. When the ith node is the first node, the prior node is the header
node.
When i >= n, the last node is the prior node and the header node is
the next node.

Doubly Linked Structures


A doubly linked structure has the advantages of a singly linked
structure. In addition, it allows the user to do the following:
•• Move left, to the previous node, from a given node.
•• Move immediately to the last node.

Figure 7-15 shows a doubly linked structure that contains three


nodes. Note the presence of two pointers, conventionally known as
next and previous, in each node. Note also the presence of a second
external tail pointer that allows direct access to the last node in the
structure.

Fig. 7-15 A doubly linked list with three nodes

Second Level Students 2021/2022 241


Data Structures and Algorithms with Python

class Node(object):
def __init__(self, data, next = None):
"""Instantiates a Node with default next of None"""
self.data = data
self.next = next
class TwoWayNode(Node):
def __init__(self, data, previous = None, next = None):
"""Instantiates a TwoWayNode."""
Node.__init__(self, data, next)
self.previous = previous

The following program creates a doubly linked structure by adding


items to the end. The program then displays the linked structure’s
contents by starting at the last item and working backward to the
first item:
"""
Tests the TwoWayNode class.
"""
from node import TwoWayNode
# Create a doubly linked structure with one node
head = TwoWayNode(1)
tail = head
# Add four nodes to the end of the doubly linked structure
for data in range(2, 6):
tail.next = TwoWayNode(data, tail)
tail = tail.next
# Print the contents of the linked structure in reverse order
probe = tail
while probe != None:
print(probe.data)
probe = probe.previous

tail.next = TwoWayNode(data, tail)


tail = tail.next

Second Level Students 2021/2022 242


Data Structures and Algorithms with Python

The purpose of these statements is to insert a new item at the end of


the linked structure.
You can assume that there is at least one node in the linked structure
and that the tail pointer always points to the last node in the
nonempty linked structure. You must set the three pointers in the
following order:
1. The previous pointer of the new node must be aimed at the current
tail node. This is accomplished by passing tail as the second
argument to the node’s constructor.
2. The next pointer of the current tail node must be aimed at the new
node. The first assignment statement accomplishes this.
3. The tail pointer must be aimed at the new node. The second
assignment statement accomplishes this.

Fig. 7-16 Inserting an item at the end of a doubly linked list

Second Level Students 2021/2022 243


Data Structures and Algorithms with Python

Questions:

Q1) Choose the correct answer:


1- Random access supports:
a. Constant time access to data b. Linear time access to data
2- Data in a singly linked structure are contained in:
a. Cells b. Nodes
3- Most operations on singly linked structures run in:
a. Constant time b. Linear time
4- The worst-case running time of an insertion into a singly linked structure
occurs when you insert:
a. At the head of the structure b. At the tail of the structure
5- A doubly linked structure allows the programmer to move:
a. To the next node or the previous node from a given node
b. To the next node only from a given node

Second Level Students 2021/2022 244


Data Structures and Algorithms with Python

8. Trees

Exercises:
1- Write a Python program to create a Balanced Binary Search
Tree (BST) using an array (given) elements where array
elements are sorted in ascending order.
2- Write a Python program to find the closest value of a given
target value in a given non-empty Binary Search Tree (BST) of
unique values.
3- Write a Python program to check whether a given a binary tree
is a valid binary search tree (BST) or not.
Let a binary search tree (BST) is defined as follows:
The left subtree of a node contains only nodes with keys less
than the node's key.

The right subtree of a node contains only nodes with keys


greater than the node's key.

Both the left and right subtrees must also be binary search
trees.
4- Write a Python program to delete a node with the given key in a
given Binary search tree (BST). First, search for the node to be
deleted, if found, delete it.
5- Consider the following tree, traverse it using BFS algorithm,
DFS algorithms (Preorder, inorder and PostOrder)

Second Level Students 2021/2022 245


Data Structures and Algorithms with Python

6- Write a function that returns the maximum value of all the keys
in a binary tree. Assume all values are nonnegative; return -1 if
the tree is empty.
7- Write a function that returns the sum of all the keys in a binary
tree.

Second Level Students 2021/2022 246


Data Structures and Algorithms with Python

Non-Linear DS. A tree is a non-empty set one element of which is


designated the root of the tree while the remaining elements are
partitioned into non-empty sets each of which is a sub-tree of the
root.
• Elements are stored in order of insertion but don't have
indexes.

A tree T is a set of nodes storing elements such that the nodes have a
parent-child relationship that satisfies the following:
• If T is not empty, T has a special tree called the root that has no
parent.
• Each node v of T different than the root has a unique parent node
w; each node with parent w is a child of w.
• Tree is one of the most important nonlinear data
structures.
• Make algorithms much faster than using linear DS, such as
array-based or linked lists.
• In file systems, graphical user interfaces, databases, web
sites, and other computer systems.

Tree nodes have many useful properties:

Second Level Students 2021/2022 247


Data Structures and Algorithms with Python

❖ The depth of a node is the length of the path (or the number of
edges) from the root to that node.
❖ The height of a node is the longest path from that node to its
leaves.
❖ The height of a tree is the height of the root. A leaf node has no
children -- its only path is up to its parent.

Fig. 10-1 Tree

Second Level Students 2021/2022 248


Data Structures and Algorithms with Python

Second Level Students 2021/2022 249


Data Structures and Algorithms with Python

Tree Terminology
Leaf node
A node with no children is called a leaf (or external node). A node
which is not a leaf is called an internal node.
Path: A sequence of nodes n1, n2, . . ., nk, such that ni is the parent of ni
+ 1 for i = 1, 2,. . ., k - 1. The length of a path is 1 less than the number of
nodes on the path. Thus there is a path of length zero from a node to
itself.
Siblings: The children of the same parent are called siblings.
Ancestor and Descendent If there is a path from node A to node B,
then A is called an ancestor of B and B is called a descendent of A.

Subtree: Any node of a tree, with all of its descendants is a subtree.

Level The level of the node refers to its distance from the root. The
root of the tree has level 0, and the level of any other node in the tree
is one more than the level of its parent.

The maximum number of nodes at any level is 2n.

Height The maximum level in a tree determines its height. (The node
height is the length of a longest path from the node to a leaf).
Height of the tree is also called depth.

Depth of a node is the number of nodes along the path from the root
to that node.

Second Level Students 2021/2022 250


Data Structures and Algorithms with Python

Fig. 10-3 A binary tree levels

Second Level Students 2021/2022 251


Data Structures and Algorithms with Python

• A node v is external (leaf) if v has no children.


• A node v is internal if it has one or more children.

• Depth of root (T)=0

List of Lists Representation:

Parse Tree for ((7+3)∗(5−2))

Second Level Students 2021/2022 252


Data Structures and Algorithms with Python

Different Tree Types:


1. Binary Tree
2. Binary Search Tree
3. AVL tree
4. B- Tree
Binary Tree
In a binary tree, each node can have at most two children. A binary
tree is either empty or consists of a node called the root together
with two binary trees called the left subtree and the right subtree

Fig. 10-2 A binary tree

Properties of Binary Trees (BT)


Some of the important properties of a binary tree are as follows:
1. If h = height of a binary tree, then
a) Maximum number of leaves =2h.
b) Maximum number of nodes= 2h+1 -1
2. If a BT has m nodes at level l, it contains at most 2m nodes at
level l+1.

Second Level Students 2021/2022 253


Data Structures and Algorithms with Python

3. Since a BT can contain at most one node at level 0 (the root), it


can contain at most 2l node at level l.
4. The total number of edges in a fully BT with n node is n-1.

Second Level Students 2021/2022 254


Data Structures and Algorithms with Python

Code:
class Node:
def __init__(self, data):
self.left = None
self.right = None
self.data = data
def PrintTree(self):
print(self.data)
root = Node(10)
root.PrintTree()

Strictly Binary Tree

If every non-leaf node in a BT has nonempty left and right subtrees,


the tree is termed a strictly binary tree. Thus the tree of figure 10-4 (a)
is strictly binary. A strictly binary tree with n leaves always contains
2n - 1 nodes.

Second Level Students 2021/2022 255


Data Structures and Algorithms with Python

Fig. 10-4 A strictly binary tree

Full Binary Tree

A full binary tree of height h has all its leaves at level h. Alternatively;
All non-leaf nodes of a full binary tree have two children, and the leaf
nodes have no children.
A full binary tree with height h has 2h + 1 - 1 nodes. A full binary tree of
height h is a strictly binary tree all whose leaves are at level h.
For example, a full binary tree of height 3 contains 23+1 – 1 = 15 nodes.

Fig. 10-5 Full binary tree

Complete Binary Tree


A binary tree with n nodes is said to be complete if it contains all the
first n nodes of the above numbering scheme.

Second Level Students 2021/2022 256


Data Structures and Algorithms with Python

A complete binary tree of height h looks like a full binary tree down to
level h-1, and the level h is filled from left to right.

Fig. 10-6 A complete binary tree

Perfect Binary Tree

A Binary tree is Perfect Binary Tree in which all internal nodes have
two children, and all leaves are at same level.

Following are examples of Perfect Binary Trees.

Fig. 10-7 Perfect binary trees

Second Level Students 2021/2022 257


Data Structures and Algorithms with Python

A Perfect Binary Tree of height h (where height is number of nodes on


path from root to leaf) has 2h – 1 node.
Example of Perfect binary tree is ancestors in family. Keep a person at
root, parents as children, parents of parents as their children.

Balanced Binary Tree


A binary tree is balanced if height of the tree is O(Log n) where n is
number of nodes. For Example, AVL tree maintain O(Log n) height by
making sure that the difference between heights of left and right
subtrees is 1.
Red-Black trees maintain O(Log n) height by making sure that the
number of Black nodes on every root to leaf paths are same and
there are no adjacent red nodes.
Balanced Binary Search trees are performance wise good as they
provide O(log n) time for search, insert and delete.
Representation of Binary Trees
1. Array Representation of Binary Tree
2. Pointer-based (linked list) representation.

Array Representation of Binary Tree


A single array can be used to represent a binary tree.
For these nodes are numbered / indexed according to a scheme
giving 0 to root. Then all the nodes are numbered from left to right
level by level from top to bottom. Empty nodes are also numbered.
Then each node having an index i is put into the array as its ith
element.

Second Level Students 2021/2022 258


Data Structures and Algorithms with Python

Example:

Fig. 10-8 binary tree -array representation

The root 3 is the 0th element while its left-child 5 is the 1 st element of
the array.
Node 6 does not have any child so its children i.e. 7 th and 8 th element
of the array are shown as a Null value.

It is found that if n is the number or index of a node, then its left child
occurs at (2n + 1)th position and right child at (2n + 2) th position of
the array. If any node does not have any of its child, then null value is
stored at the corresponding index of the array.

The following program implements the above binary tree in an array


form. And then traverses the tree in inorder traversal.

Second Level Students 2021/2022 259


Data Structures and Algorithms with Python

# Implementation to construct a Binary Tree from parent array


# A node structure
class Node:
# A utility function to create a new node
def __init__(self, key):
self.key = key
self.left = None
self.right = None
""" Creates a node with key as 'i'. If i is root, then it changes root.
If parent of i is not created, then it creates parent first
"""
def createNode(parent, i, created, root):
# If this node is already created
if created[i] is not None:
return
# Create a new node and set created[i]
created[i] = Node(i)
# If 'i' is root, change root pointer and return

if parent[i] == -1:
root[0] = created[i] # root[0] denotes root of the tree
return
# If parent is not created, then create parent first
if created[parent[i]] is None:
createNode(parent, parent[i], created, root )
# Find parent pointer
p = created[parent[i]]
# If this is first child of parent
if p.left is None:
p.left = created[i]
# If second child
else:
Second Level Students 2021/2022 260
Data Structures and Algorithms with Python

p.right = created[i]

# Creates tree from parent[0..n-1] and returns root of the created tree
def createTree(parent):
n = len(parent)
# Create and array created[] to keep track
# of created nodes, initialize all entries as None
created = [None for i in range(n+1)]
root = [None]
for i in range(n):
createNode(parent, i, created, root)

return root[0]

#Inorder traversal of tree


def inorder(root):

if root is not None:


inorder(root.left)
print root.key,
inorder(root.right)

# Test
parent = [-1, 0, 0, 1, 1, 3, 5]
root = createTree(parent)
print "Inorder Traversal of constructed tree"
inorder(root)

Linked list Representation of Binary Tree


Binary trees can be represented by links where each node contains
the address of the left child and the right child. If any node has its left

Second Level Students 2021/2022 261


Data Structures and Algorithms with Python

or right child empty then it will have in its respective link field, a null
value. A leaf node has null value in both of its links.

Code:
# Python program to create a Complete BT from with linked list

# Linked list node

class ListNode:

# Constructor to create a new node


def __init__(self, data):
self.data = data

Second Level Students 2021/2022 262


Data Structures and Algorithms with Python

self.next = None

# Binary Tree Node structure


class BinaryTreeNode:
# Constructor to create a new node
def __init__(self, data):
self.data = data
self.left = None
self.right = None

# Class to convert the linked list to Binary Tree


class Conversion:

# Constructor for storing head of linked list


# and root for the Binary Tree
def __init__(self, data = None):
self.head = None
self.root = None

def push(self, new_data):


# Creating a new linked list node and storing data

new_node = ListNode(new_data)
# Make next of new node as head
new_node.next = self.head
# Move the head to point to new node
self.head = new_node

def convertList2Binary(self):
# Queue to store the parent nodes
q = []
# Base Case
if self.head is None:
Second Level Students 2021/2022 263
Data Structures and Algorithms with Python

self.root = None
return
# 1.) The first node is always the root node,
# and add it to the queue

self.root = BinaryTreeNode(self.head.data)
q.append(self.root)
# Advance the pointer to the next node
self.head = self.head.next

# Until th end of linked list is reached, do:


while(self.head):
# 2.a) Take the parent node from the q and
# and remove it from q
parent = q.pop(0) # Front of queue
# 2.c) Take next two nodes from the linked list.
# We will add them as children of the current
# parent node in step 2.b.
# Push them into the queue so that they will be
# parent to the future node
leftChild= None
rightChild = None
leftChild = BinaryTreeNode(self.head.data)
q.append(leftChild)
self.head = self.head.next

if(self.head):
rightChild = BinaryTreeNode(self.head.data)
q.append(rightChild)
self.head = self.head.next
# 2.b) Assign the left and right children of parent

parent.left = leftChild
parent.right = rightChild
Second Level Students 2021/2022 264
Data Structures and Algorithms with Python

def inorderTraversal(self, root):

if(root):
self.inorderTraversal(root.left)
print root.data,
self.inorderTraversal(root.right)
# Test Program

conv = Conversion()
conv.push(36)
conv.push(30)
conv.push(25)
conv.push(15)
conv.push(12)
conv.push(10)
conv.convertList2Binary()
print "Inorder Traversal of the contructed Binary Tree is:"
conv.inorderTraversal(conv.root)

Binary Tree Traversals (In what order should I visit the


tree nodes?)

Traversal means to visit each node in the tree exactly once. In


linear list, nodes are visited from first to last, but a tree being a non-
linear one we need definite rules. There are different Traversal ways
differ only in the order in which they visit the nodes.

Second Level Students 2021/2022 265


Data Structures and Algorithms with Python

How Do We Traverse a Tree?


There are two main ways to search a tree: Breadth First Search
(BFS) and Depth First Search (DFS).

1- Breadth First Search traversal go horizontally right to


left or left to right. In this example: A-B-C-D-E-F-G-H
As:

1. At level 1, the tree has the root node A.


2. At level 2, it has two nodes B and C.
3. Similarly, level 3 contains nodes D, E, F, G.
4. Finally, at level 4, nodes H, I, J are present.

So the order is:

BFS: visit the same level (level by level)

The steps to implement the breadth first search technique to traverse


the above tree are as follows:
Second Level Students 2021/2022 266
Data Structures and Algorithms with Python

1. Add the node at the root to a queue. For instance, in the above
example, the node A will be added to the queue.
2. Pop an item from the queue and print/process it.
3. This is important-- add all the children of the node popped in
step two to the queue. At this point in time, the queue will
contain the children of node A:

At this point, we've completed our processing of level 1. We would


then repeat steps 2 and 3 for B and C of level 2, and continue until the
queue is empty.

Python implementation for BFS


import collections

class Tree_Node:
def __init__(self,root_value,children_nodes):
self.value = root_value
self.children_nodes = children_nodes

def breadth_first_search(Root_Node):
queue = collections.deque()
queue.append(Root_Node.value)

while queue:
node_value = queue.popleft()
print(node_value)
children_nodes = nodes_dic[node_value]

for i in children_nodes:
if i == None:
continue
queue.append(i)

Second Level Students 2021/2022 267


Data Structures and Algorithms with Python

2- Depth First Search (DFS) Traversal


In Depth First Search (DFS), a tree is traversed vertically from top

to bottom or bottom to top. As you might infer from its namesake, we

will traverse as deeply as possible, before moving on to a neighbor

node. There are three main ways to apply Depth First Search to a
tree.

➢ Inorder Traversal
➢ Preorder Traversal
➢ Postorder Traversal

1- Inorder Traversal

To traverse a non- empty tree in inorder the following steps are


followed recursively.
➢ Visit the Root
➢ Traverse the left subtree
➢ Traverse the right subtree

Second Level Students 2021/2022 268


Data Structures and Algorithms with Python

2- Preorder Traversal

➢ Visit the root.


➢ Traverse the left sub-tree, i.e., call Pre-order(left-sub-tree)
➢ Traverse the right sub-tree, i.e., call Pre-order(right-sub-tree)

3- Post-order Traversal

➢ Traverse the left sub-tree, i.e., call Post-order(left-sub-tree)


➢ Traverse the right sub-tree, i.e., call Post-order(right-sub-tree)
➢ Visit the root.

The code

# Python program for tree traversals


# A class that represents an individual node in a Binary Tree

class Node:

def __init__(self,key):
self.left = None
self.right = None
self.val = key

# A function to do inorder tree traversal


def printInorder(root):
if root:
# First recur on left child
printInorder(root.left)
# then print the data of node
print(root.val),
# now recur on right child
printInorder(root.right)

# A function to do postorder tree traversal


def printPostorder(root):

Second Level Students 2021/2022 269


Data Structures and Algorithms with Python

if root:
# First recur on left child
printPostorder(root.left)
# the recur on right child
printPostorder(root.right)
# now print the data of node
print(root.val),

# A function to do postorder tree traversal


def printPreorder(root):

if root:

# First print the data of node


print(root.val),
# Then recur on left child
printPreorder(root.left)
# Finally recur on right child
printPreorder(root.right)

# Test code

root = Node(1)
root.left = Node(2)
root.right = Node(3)
root.left.left = Node(4)
root.left.right = Node(5)

print "Preorder traversal of binary tree is"


printPreorder(root)

print "\nInorder traversal of binary tree is"


printInorder(root)

print "\nPostorder traversal of binary tree is"


printPostorder(root)

Second Level Students 2021/2022 270


Data Structures and Algorithms with Python

Time Complexity: O(n2).

Or
Python Implementation for DFS
class Tree_Node:
def __init__(self,root_value,children_nodes):
self.value = root_value
self.left_child = children_nodes[0]
self.right_child = children_nodes[1]

def depth_first_search(Root_Node):
if Root_Node.value is None:
return

stack = []
stack.append(Root_Node.value)

while(len(stack) > 0):

node_value = stack.pop()
print (node_value)
children_nodes = nodes_dic[node_value]

if not(len(children_nodes) == 1 and children_nodes[0] == None):


if children_nodes[1] is not None:
stack.append(children_nodes[1])
if children_nodes[0] is not None:
stack.append(children_nodes[0])

Second Level Students 2021/2022 271


Data Structures and Algorithms with Python

nodes_dic = {"A":["B","C"],
"B":["D","E"],
"C":["F","G"],
"D":[None],
"E":["H","I"],
"F":[None],
"G":["J", None],
"H":[None],
"I":[None],
"J":[None]}

root_node_value = next(iter(nodes_dic.keys()))
root_node_children = next(iter(nodes_dic.values()))
root_node = Tree_Node(root_node_value ,root_node_children )

depth_first_search(root_node)

Second Level Students 2021/2022 272


Data Structures and Algorithms with Python

Second Level Students 2021/2022 273


Data Structures and Algorithms with Python

Second Level Students 2021/2022 274


Data Structures and Algorithms with Python

Second Level Students 2021/2022 275


Data Structures and Algorithms with Python

Second Level Students 2021/2022 276


Data Structures and Algorithms with Python

Second Level Students 2021/2022 277


Data Structures and Algorithms with Python

Second Level Students 2021/2022 278


Data Structures and Algorithms with Python

Second Level Students 2021/2022 279


Data Structures and Algorithms with Python

Second Level Students 2021/2022 280


Data Structures and Algorithms with Python

Binary Search Tree-BST

Binary Search Tree (BST), is a node-based binary tree data structure


which has the following properties:
• The left sub-tree of a node contains only nodes with keys less
than the node’s key.
• The right sub-tree of a node contains only nodes with keys
greater than the node’s key.
• The left and right sub-tree each must also be a binary search
tree.
There must be no duplicate nodes.

Second Level Students 2021/2022 281


Data Structures and Algorithms with Python

These properties of Binary Search Tree provide an ordering among


keys so that the operations like search, minimum and maximum can
be done fast. If there is no ordering, then we may have to compare
every key to search a given key.

Searching a key
To search a given key in BST, we first compare it with root, if the key
is present at root, we return root.
If key is greater than root’s key, we recur for right sub-tree of root
node.
Otherwise we recur for left sub-tree.

Code:
# A utility function to search a given key in BST
def search(root,key):
# Base Cases: root is null or key is present at root
if root is None or root.val == key:
return root

Second Level Students 2021/2022 282


Data Structures and Algorithms with Python

# Key is greater than root's key


if root.val < key:
return search(root.right,key)
# Key is smaller than root's key
return search(root.left,key)

Priority Queues
Priority Queue is an extension of queue with following properties.
1) Every item has a priority associated with it.
2) An element with high priority is dequeued before an element with
low priority.
3) If two elements have the same priority, they are served according
to their order in the queue.
A typical priority queue supports following operations.
insert(item, priority): Inserts an item with given priority.
getHighestPriority(): Returns the highest priority item.
deleteHighestPriority(): Removes the highest priority item.

Implementation priority queue


Application of Trees:
1. One reason to use trees might be because you want to store
information that naturally forms a hierarchy. For example, the file
system on a computer:

file system
———–
/ <-- root

/\

Second Level Students 2021/2022 283


Data Structures and Algorithms with Python
... home
/\
ugrad course
//|\
... cs101 cs112 cs113

2. If we organize keys in form of a tree (with some ordering e.g., BST),


we can search for a given key in moderate time (quicker than Linked
List and slower than arrays). Self-balancing search trees like AVL and
Red-Black trees guarantee an upper bound of O(logn) for search.

3) We can insert/delete keys in moderate time (quicker than Arrays


and slower than Unordered
Linked Lists). Self-balancing search trees like AVL and Red-Black trees
guarantee an upper bound of O(logn) for insertion/deletion.

4) Like Linked Lists and unlike Arrays, Pointer implementation of


trees don’t have an upper limit on number of nodes as nodes are
linked using pointers.

The following are the common uses of tree.

1. Manipulate hierarchical data.


2. Make information easy to search (see tree traversal).
3. As a workflow for compositing digital images for visual effects.
4. Router algorithms
5. file systems, graphical user interfaces, databases, web sites,
and other computer systems.
6. Next Move in games:
In Artificial intelligence game, next moves are stored using tree DS.
7. Syntax Tree in Compiler:
In compiler, every expression is converted into syntax tree format.
8. Auto correction and spell checker
Second Level Students 2021/2022 284
Data Structures and Algorithms with Python

Simple tree

Second Level Students 2021/2022 285


Data Structures and Algorithms with Python

9. Graph

Graph is a data structure that consists of following two components:


1. A finite set of vertices also called as nodes.
2. A finite set of ordered pair of the form (u, v) called as edge.

The pair is ordered because (u, v) is not same as (v, u) in case of


directed graph (di-graph). The pair of form (u, v) indicates that there
is an edge from vertex u to vertex v. The edges may contain
weight/value/cost.

Graph and its representations

Graphs are used to represent many real life applications: Graphs are
used to represent networks.
The networks may include paths in a city or telephone network or
circuit network. Graphs are also used in social networks like linkedIn,
facebook.
For example, in facebook, each person is represented with a vertex
(or node). Each node is a structure and contains information like
person id, name, gender and locale.

Following is an example undirected graph with 5 vertices.

Second Level Students 2021/2022 286


Data Structures and Algorithms with Python

The most commonly used representations of graph.

1. Adjacency Matrix
2. Adjacency List
There are other representations also like, Incidence Matrix and
Incidence List. The choice of the graph
representation is situation specific. It totally depends on the type of
operations to be performed and ease of use.

Adjacency Matrix
Adjacency Matrix is a 2D array of size V x V where V is the number of
vertices in a graph. Let the 2D array be adj[][], a slot adj[i][j] = 1
indicates that there is an edge from vertex i to vertex j.
Adjacency matrix for undirected graph is always symmetric.
Adjacency Matrix is also used to represent weighted graphs. If adj[i][j]
= w, then there is an edge from vertex i to vertex j with weight w.

The adjacency matrix for the above example graph is:

Pros: Representation is easier to implement and follow. Removing an


edge takes O(1) time. Queries like whether there is an edge from
vertex ‘u’ to vertex ‘v’ are efficient and can be done O(1).
Second Level Students 2021/2022 287
Data Structures and Algorithms with Python

Cons: Consumes more space O(V^2). Even if the graph is sparse


(contains less number of edges), it consumes the same space. Adding
a vertex is O(V^2) time.

Adjacency List
An array of linked lists is used. Size of the array is equal to number of
vertices. Let the array be array[].
An entry array[i] represents the linked list of vertices adjacent to the
ith vertex. This representation can also be used to represent a
weighted graph. The weights of edges can be stored in nodes of
linked lists.
Following is adjacency list representation of the above graph.

Adjacency List representation of the graph


Breadth First Traversal for a Graph-BFS
Breadth First Traversal (or Search) for a graph is similar to Breadth
First Traversal of a tree The only catch here is, unlike trees, graphs
may contain cycles, so we may come to the same node again. To
avoid processing a node more than once, we use a boolean visited
array.
For example, in the following graph, we start traversal from vertex 2.
When we come to vertex 0, we look for all adjacent vertices of it. 2 is
also an adjacent vertex of 0. If we don’t mark visited vertices, then 2

Second Level Students 2021/2022 288


Data Structures and Algorithms with Python

will be processed again and it will become a non-terminating


process. Breadth First Traversal of the following graph is 2, 0, 3, 1.
The time complexity of BFS is O(V + E), where V is the number of
nodes and E is the number of edges.

Second Level Students 2021/2022 289


Data Structures and Algorithms with Python

Algorithm: Breadth-First Search Traversal


BFS(V, E, s)

Here, are important rules for using BFS algorithm:

• A queue (FIFO-First in First Out) data structure is used by BFS.


• You mark any node in the graph as root and start traversing the
data from it.
• BFS traverses all the nodes in the graph and keeps dropping
them as completed.
• BFS visits an adjacent unvisited node, marks it as done, and
inserts it into a queue.
• Removes the previous vertex from the queue in case no adjacent
vertex is found.
• BFS algorithm iterates until all the vertices in the graph are
successfully traversed and marked as completed.
• There are no loops caused by BFS during the traversing of data
from any node.

Second Level Students 2021/2022 290


Data Structures and Algorithms with Python

Python 3 code
# Python3 Program to print BFS traversal
# from a given source vertex. BFS(int s)
# traverses vertices reachable from s.
from collections import defaultdict

# This class represents a directed graph


# using adjacency list representation
class Graph:

# Constructor
def __init__(self):

# default dictionary to store graph


self.graph = defaultdict(list)

# function to add an edge to graph


def addEdge(self,u,v):
self.graph[u].append(v)

# Function to print a BFS of graph


def BFS(self, s):
# Mark all the vertices as not visited

visited = [False] * (len(self.graph))

# Create a queue for BFS


queue = []

# Mark the source node as


# visited and enqueue it
queue.append(s)
visited[s] = True

while queue:

# Dequeue a vertex from


# queue and print it
s = queue.pop(0)
print (s, end = " ")

# Get all adjacent vertices of the

# dequeued vertex s. If a adjacent


# has not been visited, then mark it
# visited and enqueue it
for i in self.graph[s]:
if visited[i] == False:
queue.append(i)
visited[i] = True

Second Level Students 2021/2022 291


Data Structures and Algorithms with Python

# Test code

# Create a graph given in


# the above diagram
g = Graph()
g.addEdge(0, 1)
g.addEdge(0, 2)
g.addEdge(1, 2)
g.addEdge(2, 0)
g.addEdge(2, 3)
g.addEdge(3, 3)

print ("Following is Breadth First Traversal"


" (starting from vertex 2)")

g.BFS(2)

Output:
Following is Breadth First Traversal (starting from vertex 2)
2 0 3 1

Second Level Students 2021/2022 292


Data Structures and Algorithms with Python

Second Level Students 2021/2022 293


Data Structures and Algorithms with Python

Applications of Breadth First Traversal


1) Shortest Path and Minimum Spanning Tree for unweighted
graph In unweighted graph, the shortest path is the path with least
number of edges.
With Breadth First, we always reach a vertex from given source using
minimum number of edges. Also, in case of unweighted graphs, any
spanning tree is Minimum Spanning Tree and we can use either
Depth or Breadth first traversal for finding a spanning tree.

Second Level Students 2021/2022 294


Data Structures and Algorithms with Python

2) Peer to Peer Networks. In Peer to Peer Networks like BitTorrent,


Breadth First Search is used to find all neighbor nodes.

3) Crawlers in Search Engines: Crawlers build index using Bread


First. The idea is to start from source
page and follow all links from source and keep doing same. Depth
First Traversal can also be used for crawlers, but the advantage with
Breadth First Traversal is, depth or levels of built tree can be limited.

4) Social Networking Websites: In social networks, we can find


people within a given distance ‘k’ from a person using Breadth First
Search till ‘k’ levels.

5) GPS Navigation systems: Breadth First Search is used to find all


neighboring locations.

6) Broadcasting in Network: In networks, a broadcasted packet


follows Breadth First Search to reach all nodes.

7) In Garbage Collection: Breadth First Search is used in copying


garbage collection using Cheney’s algorithm.

8) Cycle detection in undirected graph: In undirected graphs, either


Breadth First Search or Depth First Search can be used to detect
cycle. In directed graph, only depth first search can be used.

9) Ford–Fulkerson algorithm In Ford-Fulkerson algorithm, we can


either use Breadth First or Depth First Traversal to find the maximum
flow. Breadth First Traversal is preferred as it reduces worst case time
complexity to O(VE2).

Second Level Students 2021/2022 295


Data Structures and Algorithms with Python

10) To test if a graph is Bipartite We can either use Breadth First or


Depth First Traversal.

11) Path Finding We can either use Breadth First or Depth First
Traversal to find if there is a path between two vertices.

12) Finding all nodes within one connected component: We can


either use Breadth First or Depth First Traversal to find all nodes
reachable from a given node.

Graph Algorithms
Depth First Traversal for a Graph-DFT
Depth First Traversal (or Search) for a graph is similar to Depth First
Traversal of a tree. The only catch here is, unlike trees, graphs may
contain cycles, so we may come to the same node again. To avoid
processing a node more than once, we use a boolean visited array.
For example, in the following graph, we start traversal from vertex 2.
When we come to vertex 0, we look for all adjacent vertices of it. 2 is
also an adjacent vertex of 0. If we don’t mark visited vertices, then 2
will be processed again and it will become a non-terminating
process. Depth First Traversal of the following graph is 2, 0, 1, 3

Second Level Students 2021/2022 296


Data Structures and Algorithms with Python

Second Level Students 2021/2022 297


Data Structures and Algorithms with Python

Algorithm Depth-First Search


The DFS forms a depth-first forest comprised of more than one
depth-first trees. Each tree is made of edges (u, v) such that u is gray
and v is white when edge (u, v) is explored. The following pseudocode
for DFS uses a global timestamp time.

DFS (V, E)

???

Applications of Depth First Search


Depth-first search (DFS) is an algorithm (or technique) for traversing
a graph.
Following are the problems that use DFS as a building block.

1) For an unweighted graph, DFS traversal of the graph produces the


minimum spanning tree and all pair shortest path tree.
2) Detecting cycle in a graph
A graph has cycle if and only if we see a back edge during DFS. So we
can run DFS for the graph and check for back edges. (See this for
details)
3) Path Finding
We can specialize the DFS algorithm to find a path between two given
vertices u and z.

Second Level Students 2021/2022 298


Data Structures and Algorithms with Python

i) Call DFS(G, u) with u as the start vertex.


ii) Use a stack S to keep track of the path between the start vertex
and the current vertex.
iii) As soon as destination vertex z is encountered, return the path as
the contents of the stack

4) Topological Sorting

5) To test if a graph is bipartite


We can augment either BFS or DFS when we first discover a new
vertex, color it opposite its parents, and for each other edge, check it
doesn’t link two vertices of the same color. The first vertex in any
connected component can be red or black! See this for details.
6) Finding Strongly Connected Components of a graph A directed
graph is called strongly connected if there is a path from each vertex
in the graph to every other vertex. (See this for DFS based also for
finding Strongly Connected Components) C program to implement
the Graph Traversal
(a) Breadth first traversal
(b) Depth first traversal

DFS search starts from root node then traversal into left child node
and continues, if item found it stops
otherwise it continues. The advantage of DFS is it requires less
memory compare to Breadth First Search (BFS).

# Python program to print DFS traversal from a


# given given graph
from collections import defaultdict
# This class represents a directed graph using
# adjacency list representation
class Graph:
Second Level Students 2021/2022 299
Data Structures and Algorithms with Python

# Constructor
def __init__(self):
# default dictionary to store graph
self.graph = defaultdict(list)

# function to add an edge to graph


def addEdge(self,u,v):
self.graph[u].append(v)

# A function used by DFS


def DFSUtil(self,v,visited):
# Mark the current node as visited and print it
visited[v]= True
print (v),
# Recur for all the vertices adjacent to this vertex
for i in self.graph[v]:
if visited[i] == False:
self.DFSUtil(i, visited)

# The function to do DFS traversal. It uses


# recursive DFSUtil()
def DFS(self,v):
# Mark all the vertices as not visited
visited = [False]*(len(self.graph))
# Call the recursive helper function to print
# DFS traversal
self.DFSUtil(v,visited)
# Test code
# Create a graph given in the above diagram
g = Graph()
g.addEdge(0, 1)
g.addEdge(0, 2)
g.addEdge(1, 2)
g.addEdge(2, 0)
g.addEdge(2, 3)
g.addEdge(3, 3)
print ("Following is DFS from (starting from vertex 2)")
g.DFS(2)

Second Level Students 2021/2022 300


Data Structures and Algorithms with Python
BFS search starts from root node then traversal into next level of graph or tree and
continues, if item
found it stops otherwise it continues. The disadvantage of BFS is it requires more memory
compare to Depth First Search (DFS).

# Program to print BFS traversal from a given source


# vertex. BFS(int s) traverses vertices reachable
# from s.
from collections import defaultdict

# This class represents a directed graph using adjacency


# list representation
class Graph:

# Constructor
def __init__(self):
# default dictionary to store graph
self.graph = defaultdict(list)

# function to add an edge to graph


def addEdge(self,u,v):
self.graph[u].append(v)

# Function to print a BFS of graph


def BFS(self, s):

# Mark all the vertices as not visited


visited = [False]*(len(self.graph))
# Create a queue for BFS
queue = []
# Mark the source node as visited and enqueue it
queue.append(s)
visited[s] = True

while queue:
# Dequeue a vertex from queue and print it
s = queue.pop(0)
print (s)

Second Level Students 2021/2022 301


Data Structures and Algorithms with Python

# Get all adjacent vertices of the dequeued


# vertex s. If a adjacent has not been visited,
# then mark it visited and enqueue it
for i in self.graph[s]:
if visited[i] == False:
queue.append(i)
visited[i] = True

# Test code
# Create a graph given in the above diagram
g = Graph()
g.addEdge(0, 1)
g.addEdge(0, 2)
g.addEdge(1, 2)
g.addEdge(2, 0)
g.addEdge(2, 3)

g.addEdge(3, 3)

print ("Following is Breadth First Traversal (starting from vertex 2)")


g.BFS(2)

10. Recursion
A function is recursive if it calls itself and has a termination
condition. Why a termination condition? To stop the function from
calling itself to infinity.

Difference between Recursion and Iteration:


1. A function is said to be recursive if it calls itself again and again
within its body whereas iterative functions are loop based imperative
functions.
2. Recursion uses stack whereas iteration does not use stack.
3. Recursion uses more memory than iteration as its concept is based
on stacks.
Second Level Students 2021/2022 302
Data Structures and Algorithms with Python

4. Recursion is comparatively slower than iteration due to overhead


condition of maintaining stacks.
5. Recursion makes code smaller and iteration makes code longer.
6. Iteration terminates when the loop-continuation condition fails
whereas recursion terminates when a base case is recognized.
7. While using recursion multiple activation records are created on
stack for each call whereas in iteration everything is done in one
activation record.
8. Infinite recursion can crash the system whereas infinite looping
uses CPU cycles repeatedly.

Recursion can be classified into:


1- Direct recursion- if a function calls itself from within itself.
2- Indirect recursion- if two functions call one another mutually.
Recursion may be further categorized as:
• Linear Recursion
• Binary Recursion
• Multiple Recursion

A linear recursive function is a function that only makes a single


call to itself each time the function runs. The factorial function is a
good example of linear recursion.

 A binary recursive function is a function that calls itself twice.


For example, Fibonacci sequence fib(n)=fib(n-1) + fib(n-2), n>2.

 A multiple recursive function is a function that calls itself


multiple times. For example, tree traversal such as in depth -first
search.

Recursion examples:

Second Level Students 2021/2022 303


Data Structures and Algorithms with Python

Example 1: Simple example:

Adding all numbers in a list. Without recursion, this could be:

# python Sum Example NO recursion


def sum(list):
sum = 0

# Add every number in the list.


for i in range(0, len(list)):
sum = sum + list[i]

# Return the sum.


return sum

print(sum([5,7,3,8,10]))

To do this recursively:

If the length of the list is


# python Sum Example with recursion one it returns the list (the
termination condition).
def sum(list): Else, it returns the
if len(list) == 1: element and a call to the
return list[0] function sum() minus one
element of the list. If all
else: calls are executed, it
return list[0] + sum(list[1:]) returns reaches the
termination condition and
print(sum([5,7,3,8,10])) returns the answer.

Example 2: Factorial with recursion

The mathematical definition of factorial is: n! = n * (n-1)!, if n > 1 and


f(1) = 1. Example: 3! = 3 x 2 x 1 = 6. We can implement this in
Python using a recursive function:

def factorial(n):
if n == 1:
return 1

Second Level Students 2021/2022 304


Data Structures and Algorithms with Python

else:
return n * factorial(n-1)

print(factorial(3))

When calling the factorial function n = 3. Thus, it returns n *


factorial(n-1). This process will continue until n = 1. If n==1 is
reached, it will return the result.

Example:

def tri_recursion(k):
if(k>0):
result = k+tri_recursion(k-1)
print(result)
else:
result = 0
return result

print("\n\nRecursion Example Results")


tri_recursion(6)

Limitations of recursions

Everytime a function calls itself and stores some memory. Thus, a


recursive function could hold much more memory than a traditional
function. Python stops the function calls after a depth of 1000 calls. If
you run this example:

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

print(factorial(3000))

You will get the error:

RuntimeError: maximum recursion depth exceeded

Second Level Students 2021/2022 305


Data Structures and Algorithms with Python

In other programming languages, your program could simply crash.


You can resolve this by modifying the number of recursion calls such
as:
import sys

sys.setrecursionlimit(5000)

def factorial(n):
if n == 1:
return 1
else:
return n*factorial(n 1)

print(factorial(3000))

but keep in mind there is still a limit to the input for the factorial
function. For this reason, you should use recursion wisely. As you
learned now for the factorial problem, a recursive function is not the
best solution. For other problems such as traversing a directory,
recursion may be a good solution.

Questions:

1. Compute SE (step execution) and big O for the following code:


for(i=0; i<n; i++)
a[i]=0;
for(j=0; j<n; j++)
for(k=0; k<n; k++)
a[j]+=a[k]+j+k;

5. Consider the following algorithm:


for i = 0 To <largest index> - 1
// Loop
for j = i + 1 To <largest index>
// See if these two items are duplicates.
If (array[i] == array[j]) Then Return True
Next j
Next i
// If we get to this point, there are no duplicates.

Second Level Students 2021/2022 306


Data Structures and Algorithms with Python

Return False
What is the run time of this new version?

Sol:
The outer loop still executes O (N) times in the algorithm.
When the outer loop’s counter is i, the inner loop executes O (N – i) times. If you add
up the number of times that the inner loop executes, the
( )
result is N + (N – 1) + (N – 2) +…+ 1 = N (N – 1)/2 = N2 – N /2. This
is still O (N2) ,

Second Level Students 2021/2022 307


Data Structures and Algorithms with Python

11. BINARY TREES AND HASHING

Binary Search Trees

An important special kind of binary tree is the binary search tree


(BST). In a BST, each node stores some information including a
unique key value, and perhaps some associated data. A binary tree is
a BST iff, for every node n in the tree:
✓ All keys in n's left subtree are less than the key in n, and
✓ All keys in n's right subtree are greater than the key in n.
In other words, binary search trees are binary trees in which all
values in the node’s left subtree are less than node value all values in
the node’s right subtree are greater than node value.
Here are some BSTs in which each node just stores an integer key:

These are not BSTs:

In the left one 5 is not greater than 6. In the right one 6 is not greater
than 7.
The reason binary-search trees are important is that the following
operations can be implemented efficiently using a BST:
Second Level Students 2021/2022 308
Data Structures and Algorithms with Python

✓ insert a key value


✓ determine whether a key value is in the tree
✓ remove a key value from the tree
✓ print all of the key values in sorted order

Let's illustrate what happens using the following BST:

and searching for 12:


12<13 so go to left subtree.

12>9 so go to right subtree.

Second Level Students 2021/2022 309


Data Structures and Algorithms with Python

What if we search for 15:


15> 13 so go to right subtree.

15< 16 so go to left subtree. It does not exist so search fails and it


returns false.

BST Properties

BST is a BT ordered in the following way:


1. Each node contains one key (unique).
2. The keys in the left subtree are less than the key in its parent
node.

Second Level Students 2021/2022 310


Data Structures and Algorithms with Python

3. The keys in the right subtree are greater than the key in its
parent node.
4. Duplicate node keys are not allowed.

Inserting a node
A naïve algorithm for inserting a node into a BST is that, we start from
the root node, if the node to insert is less than the root, we go to left
child, and otherwise we go to the right child of the root. We continue
this process (each node is a root for some sub tree) until we find a
null pointer (or leaf node) where we cannot go any further. We then
insert the node as a left or right child of the leaf node based on node
is less or greater than the leaf node. We note that a new node is
always inserted as a leaf node. A recursive algorithm for inserting a
node into a BST is as follows. Assume we insert a node N to tree T. if
the tree is empty, we return new node N as the tree. Otherwise, the
problem of inserting is reduced to inserting the node N to left of right
sub trees of T, depending on N is less or greater than T. A definition is
as follows.
Insert (N, T)= N if T is empty
= insert(, T.left) if N< T
=insert(N, T.right) if N> T

Searching for a node


Searching for a node is similar to inserting a node. We start from root,
and then go left or right until we find (or not find the node). A
recursive definition of search is as follows. If the node is equal to root,
then we return true. If the root is null, then we return false. Otherwise

Second Level Students 2021/2022 311


Data Structures and Algorithms with Python

we recursively solve the problem for T.left or T.right, depending on N


< T or N > T.
A recursive definition is as follows.
Search should return a true or false, depending on the node is found
or not.
Search(N, T) = false if T is empty
= true if T = N
= search(N, T.left) if N < T
= search(N, T.right) if N > T

Deleting a node
A BST is a connected structure. That is, all nodes in a tree are
connected to some other node. For example, each node has a parent,
unless node is the root. Therefore deleting a node could affect all sub
trees of that node. For example, deleting node 5 from the tree could
result in losing sub trees that are rooted at 1 and 9.

Hence we need to be careful about deleting nodes from a tree. The


best way to deal with deletion seems to be considering special cases.
What if the node to delete is a leaf node?
What if the node is a node with just one child? What if the node is an
internal node (with two children). The latter case is the hardest to
resolve. But we will find a way to handle this situation as well.

Second Level Students 2021/2022 312


Data Structures and Algorithms with Python

Case 1 : The node to delete is a leaf node


This is a very easy case. Just delete the node 46. We are done

Case 2 : The node to delete is a node with one child.


This is also not too bad. If the node to be deleted is a left child of the
parent, then we connect the left pointer of the parent (of the deleted
node) to the single child. Otherwise if the node to be deleted is a right
child of the parent, then we connect the right pointer of the parent
(of the deleted node) to single child.

Case 3: The node to delete is a node with two children


This is a difficult case as we need to deal with two sub trees. But we
find an easy way to handle it. First we find a replacement node (from
leaf node or nodes with one child) for the node to be deleted. We
need to do this while maintaining the BST order property. Then we
swap leaf node or node with one child with the node to be deleted
(swap the data) and delete the leaf node or node with one child (case
1 or case 2)
Next problem is finding a replacement leaf node for the node to be
deleted. We can easily find this as follows. If the node to be deleted is
Second Level Students 2021/2022 313
Data Structures and Algorithms with Python

N, the find the largest node in the left sub tree of N or the smallest
node in the right sub tree of N. These are two candidates that can
replace the node to be deleted without losing the order property. For
example, consider the following tree and suppose we need to delete
the root 38.

Then we find the largest node in the left sub tree (15) or smallest
node in the right sub tree (45) and replace the root with that node
and then delete that node. The following set of images demonstrates
this process.

Let’s see when we delete 13 from that tree.

Into right subtree


Second Level Students 2021/2022 314
Data Structures and Algorithms with Python

Go to left child

Go to left child (last one)

Replace node to delete with far left child of right subtree.

Remove far left child of right subtree.

Second Level Students 2021/2022 315


Data Structures and Algorithms with Python

Balanced Search Trees


A self-balancing (or height-balanced) binary search tree is any
node-based binary search tree that automatically keeps its height
(maximal number of levels below the root) small in the face of
arbitrary item insertions and deletions. The red–black tree, which is a
type of self-balancing BST, was called symmetric binary B-tree.
Self-balancing binary search trees can be used in a natural way to
construct and maintain ordered lists, such as priority queues. They
can also be used for associative arrays; key-value pairs are simply
inserted with an ordering based on the key alone. In this capacity,
self-balancing BSTs have a number of advantages and disadvantages
over their main competitor, hash tables.
One advantage of self-balancing BSTs is that they allow fast (indeed,
asymptotically optimal) enumeration of the items in key order, which
hash tables do not provide. One disadvantage is that their lookup
algorithms get more complicated when there may be multiple items
with the same key.
Self-balancing BSTs have better worst-case lookup performance than
hash tables (O(log n) compared to O(n)), but have worse average-
case performance (O(log n) compared to O(1)).

Self-balancing BSTs can be used to implement any algorithm that


requires mutable ordered lists, to achieve optimal worst-case
asymptotic performance.
For example, if binary tree sort is implemented with a self-balanced
BST, we have a very simple-to-describe yet asymptotically optimal
O(n log n) sorting algorithm. Similarly, many algorithms in
computational geometry exploit variations on self-balancing BSTs to
solve problems such as the line segment intersection problem and

Second Level Students 2021/2022 316


Data Structures and Algorithms with Python

the point location problem efficiently. (For average-case


performance, however, self-balanced BSTs may be less efficient than
other solutions.
Binary tree sort, in particular, is likely to be slower than merge sort,
quicksort, or heapsort, because of the tree-balancing overhead as
well as cache access patterns.)
Self-balancing BSTs are flexible data structures, in that it's easy to
extend them to efficiently record additional information or perform
new operations. For example, one can record the number of nodes in
each subtree having a certain property, allowing one to count the
number of nodes in a certain key range with that property in O(log n)
time. These extensions can be used, for example, to optimize
database queries or other list-processing algorithms.

AVL Trees
An AVL tree is another balanced BST. Like red-black trees, they are
not perfectly balanced, but pairs of sub-trees differ in height by at
most 1, maintaining an O(logn) search time. Addition and deletion
operations also take O(logn) time.

Definition of an AVL tree: An AVL tree is a binary search tree which


has the following properties:

1. The sub-trees of every node differ in height by at most one.


2. Every sub-tree is an AVL tree.

Second Level Students 2021/2022 317


Data Structures and Algorithms with Python

Balance
requirement
for an AVL
tree: the left
and right
sub-trees
differ by at
most 1 in
height.

For example, here are some trees:

Yes, this is an AVL tree. Examination shows that each left sub-tree has
a height 1 greater than each right sub-tree.

No this is not an AVL tree. Sub-tree with root 8 has height 4 and sub-
tree with root 18 has height 2.
An AVL tree implements the Map abstract data type just like a regular
binary search tree, the only difference is in how the tree performs. To

Second Level Students 2021/2022 318


Data Structures and Algorithms with Python

implement our AVL tree we need to keep track of a balance factor for
each node in the tree. We do this by looking at the heights of the left
and right subtrees for each node. More formally, we define the
balance factor for a node as the difference between the height of the
left subtree and the height of the right subtree.
balanceFactor=height(leftSubTree)−height(rightSubTree)

Using the definition for balance factor given above we say that a
subtree is left-heavy if the balance factor is greater than zero. If the
balance factor is less than zero, then the subtree is right heavy.
If the balance factor is zero, then the tree is perfectly in balance. For
purposes of implementing an AVL tree and gaining the benefit of
having a balanced tree we will define a tree to be in balance if the
balance factor is -1, 0, or 1.
Once the balance factor of a node in a tree is outside this range, we
will need to have a procedure to bring the tree back into balance.
Figure shows an example of an unbalanced, right-heavy tree and the
balance factors of each node.

Second Level Students 2021/2022 319


Data Structures and Algorithms with Python

Properties of AVL Trees


AVL trees are identical to standard binary search trees except that for
every node in an AVL tree, the height of the left and right subtrees can
differ by at most 1.
AVL trees are HB-k trees (height balanced trees of order k) of order
HB-1.
The following is the height differential formula:
| Height(TL)- Height(TR) | ≤k

When storing an AVL tree, a field must be added to each node with
one of three values: 1, 0, or -1. A value of 1 in this field means that the
left subtree has a height one more than the right subtree.
A value of -1 denotes the opposite. A value of 0 indicates that the
heights of both subtrees are the same. Updates of AVL trees require
up to O(log n) rotations, whereas updating red-black trees can be
done using only one or two rotations (up to O(log n) color changes).
For this reason, they (AVL trees) are considered a bit obsolete by
some.

Second Level Students 2021/2022 320


Data Structures and Algorithms with Python

Sparse AVL trees


Sparse AVL trees are defined as AVL trees of height h with the fewest possible
nodes. Figure 3 shows sparse AVL trees of heights 0, 1, 2, and 3.

Introduction to M-Way Search Trees


A multiway tree is a tree that can have more than two children. A
multiway tree of order m (or an m-way tree) is one in which a tree
can have m children.
As with the other trees that have been studied, the nodes in an m-
way tree will be made up of key fields, in this case m-1 key fields, and
pointers to children.
Multiday tree of order 5

To make the processing of m-way trees easier some type of order will
be imposed on the keys within each node, resulting in a multiway
search tree of order m (or an m-way search tree).
By definition an m-way search tree is a m-way tree in which:

Second Level Students 2021/2022 321


Data Structures and Algorithms with Python

✓ Each node has m children and m-1 key fields


✓ The keys in each node are in ascending order.
✓ The keys in the first i children are smaller than the ith key
✓ The keys in the last m-i children are larger than the ith key
4-way search tree

M-way search trees give the same advantages to m-way trees that
binary search trees gave to binary trees - they provide fast
information retrieval and update. However, they also have the same
problems that binary search trees had - they can become
unbalanced, which means that the construction of the tree becomes
of vital importance.

B Trees
An extension of a multiway search tree of order m is a B-tree of order
m. This type of tree will be used when the data to be accessed/stored
is located on secondary storage devices because they allow for large
amounts of data to be stored in a node.
A B-tree of order m is a multiway search tree in which:
1. The root has at least two subtrees unless it is the only node in the
tree.
Second Level Students 2021/2022 322
Data Structures and Algorithms with Python

2. Each non-root and each non-leaf node have at most m nonempty


children and at least m/2 nonempty children.
3. The number of keys in each non-root and each non-leaf node is
one less than the number of its nonempty children.
4. All leaves are on the same level.
These restrictions make B-trees always at least half full, have few
levels, and remain perfectly balanced.

Searching a B-tree
An algorithm for finding a key in B-tree is simple. Start at the root and
determine which pointer to follow based on a comparison between
the search value and key fields in the root node.
Follow the appropriate pointer to a child node. Examine the key
fields in the child node and continue to follow the appropriate
pointers until the search value is found or a leaf node is reached that
does not contain the desired search value.

Insertion into a B-tree

The condition that all leaves must be on the same level forces a
characteristic behavior of B-trees, namely that B-trees are not
allowed to grow at the their leaves; instead they are forced to grow at
the root.
When inserting into a B-tree, a value is inserted directly into a leaf.
This leads to three common situations that can occur:
1. A key is placed into a leaf that still has room.
2. The leaf in which a key is to be placed is full.
3. The root of the B-tree is full.

Case 1: A key is placed into a leaf that still has room

Second Level Students 2021/2022 323


Data Structures and Algorithms with Python

This is the easiest of the cases to solve because the value is simply
inserted into the correct sorted position in the leaf node.

Inserting the number 7 results in:

Case 2: The leaf in which a key is to be placed is full


In this case, the leaf node where the value should be inserted is split
in two, resulting in a new leaf node. Half of the keys will be moved
from the full leaf to the new leaf. The new leaf is then incorporated
into the B-tree.
The new leaf is incorporated by moving the middle value to the
parent and a pointer to the new leaf is also added to the parent. This
process is continuing up the tree until all of the values have "found" a
location.
Insert 6 into the following B-tree:

results in a split of the first leaf node:

Second Level Students 2021/2022 324


Data Structures and Algorithms with Python

The new node needs to be incorporated into the tree - this is


accomplished by taking the middle value and inserting it in the
parent:

Case 3: The root of the B-tree is full


The upward movement of values from case 2 means that it's possible
that a value could move up to the root of the B-tree. If the root is full,
the same basic process from case 2 will be applied and a new root
will be created. This type of split results in 2 new nodes being added
to the B-tree.
Inserting 13 into the following tree:

Results in:
Second Level Students 2021/2022 325
Data Structures and Algorithms with Python

The 15 needs to be moved to the root node but it is full. This means
that the root needs to be divided:

The 15 is inserted into the parent, which means that it becomes the
new root node:

Deleting from a B-tree


The deletion process will basically be a reversal of the insertion
process - rather than splitting nodes, it's possible that nodes will be
merged so that B-tree properties, namely the requirement that a
node must be at least half full, can be maintained.
There are two main cases to be considered:
1. Deletion from a leaf
2. Deletion from a non-leaf

Second Level Students 2021/2022 326


Data Structures and Algorithms with Python

Case 1: Deletion from a leaf


1a) If the leaf is at least half full after deleting the desired value, the
remaining larger values are moved to "fill the gap".
Deleting 6 from the following tree:

results in:

1b) If the leaf is less than half full after deleting the desired value
(known as underflow), two things could happen:
Deleting 7 from the tree above results in:

1b-1) If there is a left or right sibling with the number of keys


exceeding the minimum requirement, all of the keys from the leaf
and sibling will be redistributed between them by moving the

Second Level Students 2021/2022 327


Data Structures and Algorithms with Python

separator key from the parent to the leaf and moving the middle key
from the node and the sibling combined to the parent.

Now delete 8 from the tree:

1b-2) If the number of keys in the sibling does not exceed the
minimum requirement, then the leaf and sibling are merged by
putting the keys from the leaf, the sibling, and the separator from
the parent into the leaf.
The sibling node is discarded and the keys in the parent are moved to
"fill the gap". It's possible that this will cause the parent to
underflow. If that is the case, treat the parent as a leaf and continue
repeating step 1b-2 until the minimum requirement is met or the root
of the tree is reached.

Special Case for 1b-2:


When merging nodes, if the parent is the root with only one key, the
keys from the node, the sibling, and the only key of the root are
placed into a node and this will become the new root for the B-tree.
Both the sibling and the old root will be discarded.

Second Level Students 2021/2022 328


Data Structures and Algorithms with Python

Case 2: Deletion from a non-leaf


This case can lead to problems with tree reorganization but it will be
solved in a manner similar to deletion from a BST.
The key to be deleted will be replaced by its immediate predecessor
(or successor) and then the predecessor (or successor) will be
deleted since it can only be found in a leaf node.
Deleting 16 from the tree above results in:

The "gap" is filled in with the immediate predecessor:

and then the immediate predecessor is deleted:

Second Level Students 2021/2022 329


Data Structures and Algorithms with Python

If the immediate successor had been chosen as the replacement:

Deleting the successor results in:

The vales in the left sibling are combined with the separator key (18)
and the remaining values.
They are divided between the 2 nodes:

and then the middle value is moved to the parent:

Second Level Students 2021/2022 330


Data Structures and Algorithms with Python

Hashing and Collision


Hashing is the technique used for performing almost constant time
search in case of insertion, deletion and find operation.
Taking a very simple example of it, an array with its index as key is
the example of hash table. So, each index (key) can be used for
accessing the value in a constant search time. This mapping key must
be simple to compute and must helping in identifying the associated
value. Function which helps us in generating such kind of key-value
mapping is known as Hash Function.
In a hashing system the keys are stored in an array which is called the
Hash Table. A perfectly implemented hash table would always
promise an average insert/delete/retrieval time of O(1).

Hashing Function
A function which employs some algorithm to computes the key K for
all the data elements in the set U, such that the key K which is of a
fixed size.
The same key K can be used to map data to a hash table and all the
operations like insertion, deletion and searching should be possible.
The values returned by a hash function are also referred to as hash
values, hash codes, hash sums, or hashes.

Second Level Students 2021/2022 331


Data Structures and Algorithms with Python

Hash Collision
A situation when the resultant hashes for two or more data elements
in the data set U, maps to the same location in the has table, is called
a hash collision. In such a situation two or more data elements would
qualify to be stored / mapped to the same location in the hash table.

Second Level Students 2021/2022 332


Data Structures and Algorithms with Python

Hash collision resolution techniques

Open Hashing (Separate chaining)


Open hashing is a technique in which the data is not directly stored
at the hash key index (k) of the Hash table.
Rather the data at the key index (k) in the hash table is a pointer to
the head of the data structure where the data is actually stored.
In the most simple and common implementations the data structure
adopted for storing the element is a linked-list.

In this technique when a data needs to be searched, it might become


necessary (worst case) to traverse all the nodes in the linked list to
retrieve the data.
Note that the order in which the data is stored in each of these linked
lists (or other data structures) is completely based on
implementation requirements. Some of the popular criteria are
insertion order, frequency of access etc.

Second Level Students 2021/2022 333


Data Structures and Algorithms with Python

Closed hashing (open Addressing)


In this technique a hash table with pre-identified size is considered.
All items are stored in the hash table itself.
In addition to the data, each hash bucket also maintains the three
states: EMPTY, OCCUPIED, DELETED.
While inserting, if a collision occurs, alternative cells are tried until an
empty bucket is found. For which one of the following techniques is
adopted.

1. Liner Probing
2. Quadratic probing
3. Double hashing (in short in case of collision another hashing
function is used with the key value as an input to identify where in
the open addressing scheme the data should actually be stored.)

A comparative analysis of Closed Hashing vs Open Hashing

Open Addressing Closed Addressing


All elements would be stored in the Additional data structure needs to be
Hash table itself. No additional structure used to accommodate collision data.
is needed.
In cases of collisions, a unique hash key
Simple and effective approach to
must be obtained. collision resolution. Key may or may not
be unique.
Determining size of the hash table, Performance deterioration of closed
adequate for storing all the data is addressing much slower as compared to
difficult. Open addressing.
State needs be maintained for the data No state data needs to be maintained
(additional work). (easier to maintain).
Use space efficiently. Expensive on space.

Second Level Students 2021/2022 334


Data Structures and Algorithms with Python

Applications of Hashing

A hash function maps a variable length input string to fixed


length output string -- its hash value, or hash for short.
 If the input is longer than the output, then some inputs must map
to the same output – a hash collision.
Comparing the hash values for two inputs can give us one of two
answers: the inputs are not the same, or there is a possibility that
they are the same. Hashing as we know it is used for performance
improvement, error checking, and authentication.

In the common hash table, which uses a hash function to index
into the correct bucket in the hash table, followed by comparing each
element in the bucket to find a match. In error checking,
hashes (checksums, message digests, etc.) are used to detect errors
caused by either hardware or software.

Examples are TCP checksums, ECC memory, and MD5 checksums


on downloaded files. In
this case, the hash provides additional assurance that the data we
received is correct.
Finally, hashes are used to authenticate messages, this means
trying to protect the original input from tampering and selecting a
hash that is strong enough to make malicious attack infeasible or
unprofitable.

➢ Construct a message authentication code (MAC)


➢ Digital signature
➢ Make commitments, but reveal message later
➢ Timestamping

Second Level Students 2021/2022 335


Data Structures and Algorithms with Python

➢ Key updating: key is hashed at specific intervals resulting in


new key

References

1. Narasimha Karumanchi,”Data Structures And Algorithmic Thinking With


Python”, CareerMonk. Com, 2016
2. Kenneth A. Lambert, “Fundamentals of Python®: Data Structures”, 2019
3. Michael T. Goodrich and Roberto Tamassia, “Algorithm Design and
Applications”, WILEY, 2015
4. Rod Stephens, ”Essential Algorithms: A Practical Approach to Computer
Algorithms Using Python® and C#”, WILEY, 2019.

WEB References

1. https://docs.python.org/3/tutorial/datastructures.html

2. http://www.tutorialspoint.com/data_structures_algorithms

Second Level Students 2021/2022 336

You might also like