Thanks to visit codestin.com
Credit goes to www.scribd.com

0% found this document useful (0 votes)
11 views102 pages

Lec08 Sort Scale

The document discusses sorting algorithms and their efficiency. It covers insertion sort, compares different implementations of searching a list, and introduces the concept of analyzing algorithms based on the number of operations they perform rather than actual runtime.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
11 views102 pages

Lec08 Sort Scale

The document discusses sorting algorithms and their efficiency. It covers insertion sort, compares different implementations of searching a list, and introduces the concept of analyzing algorithms based on the number of operations they perform rather than actual runtime.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 102

Iteration:

Sorting, Scalability, Big O Notation

1
Announcements

¤ Yesterday?
¤ Lab 4

¤ Tonight
¤ Lab 5

¤ Tomorrow
¤ PS 4
¤ PA 4
Yesterday

¤ Quick Review: Sieve of Eratosthenes

¤ Character Comparisons (Unicode)

¤ Linear Search

¤ Sorting
Today

¤ Review: Insertion Sort

¤ Scalability

¤ Big O Notation
Sorting

5
In-place Insertion Sort

¤ Idea: during sorting, a prefix of the list is already sorted. (This


prefix might contain one, two, or more elements.)

¤ Each element that we process is inserted into the correct


place in the sorted prefix of the list.

¤ Result: sorted part of the list gets bigger until the whole
thing is sorted.
In-place Insertion Sort

sorted part
In-place Insertion Sort

sorted part
In-place Insertion Sort

sorted part

15110 Principles of Computing 9


Carnegie Mellon University
In-place Insertion Sort

sorted part
In-place Insertion Sort

sorted part
In-place Insertion Sort

sorted part
In-place Insertion Sort

sorted part
In-place Insertion Sort

sorted part
In-place Insertion Sort

sorted part
In-place Insertion Sort

sorted part
In-place Insertion Sort

sorted part
In-place Insertion Sort

sorted part
In-place Insertion Sort

sorted part
In-place Insertion Sort

sorted part
In-place Insertion Sort

sorted part
In-place Insertion Sort Algorithm
Given a list a of length n, n > 0.

1. Set i = 1.

2. While i is not equal to n, do the following:

a. Insert a[i] into its correct position in a[0] to a[i] (inclusive).

b. Add 1 to i.

3. Return the list a (which is now sorted).


Example

a = [53, 26, 76, 30, 14, 91, 68, 42]


i = 1

Insert a[1] into its correct position in a[0..1]


and then add 1 to i:
53 moves to the right,
26 is inserted into the list at position 0
a = [26, 53, 76, 30, 14, 91, 68, 42]
i = 2
Writing the Python code
def isort(items):

i = 1

while i < len(items):

move_left(items, i)
insert a[i] into a[0..i]
i = i + 1 in its correct sorted
position
return items
Moving left using search
To move the element x at index i “left” to its
correct position, remove it, start at position i-
1, and search from right to left until we find the
first element that is less than or equal to x.
Then insert x back into the list to the right of
that element.
(The Python insert operation does not
overwrite. Think of it as “squeezing into the
list”.)
Moving left (numbers)
76:

a = [26, 53, 76, 30, 14, 91, 68, 42]

Searching from right to left starting with 53, the first element less than 76 is 53.
Insert 76 to the right of 53 (where it was before).

14:

a = [26, 30, 53, 76, 14, 91, 68, 42]

Searching from right to left starting with 76, all elements left of 14 are greater
than 14. Insert 14 into position 0.

68:

a = [14, 26, 30, 53, 76, 91, 68, 42]

Searching from right to left starting with 91, the first element less than 68 is 53.

Insert 68 to the right of 53.


The move_left algorithm

Given a list a of length n, n > 0 and a value at


index i to be moved left in the list.
1. Remove a[i] from the list and store in x.
2. Set j = i-1.
3. While j >= 0 and a[j] > x, subtract 1 from j.
4. (At this point, what do we know? Either j is …,
or a[j] is …) Insert x into position a[j+1].
Removing a list element: pop
>>> a = ["Wednesday", "Monday", "Tuesday"]
>>> day = a.pop(1)
>>> a
['Wednesday', 'Tuesday']
>>> day
'Monday'
>>> day = a.pop(0)
>>> day
'Wednesday'
>>> a
['Tuesday']
Inserting an element: insert

>> a = [10, 20, 30]


=> [10, 20, 30]
>> a.insert(0, "foo")
=> ["foo", 10, 20, 30]
>> a.insert(2, "bar")
=> ["foo", 10, "bar", 20, 30]
>> a.insert(5, "baz")
=> ["foo", 10, "bar", 20, 30, "baz"]
move_left in Python

def move_left(items, i):

x = items.pop(i)

j = i - 1

while j >= 0 and items[j] > x:


j = j – 1

items.insert(j + 1, x)
Problems, Algorithms and Programs

¤ One problem : potentially many algorithms

¤ One algorithm : potentially many programs

¤ We can compare how efficient different programs are


both analytically and empirically

31
Analytically: Which One is Faster?

def contains1(items, key): def contains2(items, key):

ln = len(items)
index = 0
index = 0
while index < len(items):
while index < ln:
if items[index] == key:
if items[index] == key:
return True
return True
index = index + 1
index = index + 1
return False
return False
¤ len(items) is executed each
len(items) is executed only
time loop condition is checked
once and its value is stored in ln
32
Is a for-loop faster than a while-loop?

•Add the following function to our collection of


contains functions from the previous page:

def contains3(items, key):


for index in range(len(items)):
if items[index] == key:
return True
return False

33
Empirical Measurement
¤ Three programs for the same algorithm; let’s measure which is faster:

¤ Define time2 and time3 similarly to call contains2 and contains

import time
def time1(items, key) :
start = time.time()
contains1(items, key)
runtime = time.time() - start
print("contains1:", runtime)

34
Doing the measurement
>>> items = [None] * 1000000

>>> time1(items1, 1)

contains1: 0.1731700897216797

>>> time2(items1, 1)

contains2: 0.1145467758178711

>>> time3(items1, 1)

contains3: 0.07184195518493652

Conclusion: using for and range() is faster than using


while and addition when doing an unsuccessful
search Why?

35
A Different Measurement
¤ What if we want to know how the different loops perform when the
key matches the first element?

>>> time1(items1, None)

contains1: 4.0531158447265625e-06

>>> time2(items1, None)

contains2: 4.291534423828125e-06

>>> time3(items1, None)

contains3: 1.0013580322265625e-05

Now the relationship is different; contains3 is


slowest! Why?

36
Thinking like a computer scientist
Code Analysis
Efficiency
¤ A computer program should be correct, but it should
also
¤ execute as quickly as possible (time-efficiency)
¤ use memory wisely (storage-efficiency)
¤ How do we compare programs (or algorithms in
general) with respect to execution time?
¤ various computers run at different speeds due to different
processors
¤ compilers optimize code before execution
¤ the same algorithm can be written differently depending
on the programming paradigm
Counting Operations
¤ We measure time efficiency by considering “work”
done
¤ Counting the number of operations performed by the
algorithm.

¤ But what is an “operation”? Think of it in a


machine-independent way
¤ assignment statements
¤ comparisons
¤ function calls
¤ return statements
¤ We think of an operation as any computation that is
independent of the size of our input.
Linear Search
# let n = the length of list.
def search(list, key):
index = 0
while index < len(list):
if list[index] == key:
return index
index = index + 1 Best case: the key is the first
element in the list
return None
Linear Search: Best Case

# let n = the length of list.


def search(list, key):
index = 0 1
while index < len(list): 1
if list[index] == key: 1
return index 1
index = index + 1
return None
Total:4
Linear Search: Worst Case
# let n = the length of list.
def search(list, key):
index = 0
while index < len(list):
if list[index] == key:
return index
index = index + 1 Worst case: the key is not an
element in the list
return None
Linear Search: Worst Case
# let n = the length of list.
def search(list, key):
index = 0 1
while index < len(list): n+1
if list[index] == key: n
return index
index = index + 1 n
return None 1
Total: 3n+3
Asymptotic Analysis
¤How do we know that each operation we count takes
the same amount of time?
¤ We don’t.
¤So generally, we look at the process more abstractly
¤ We care about the behavior of a program in the long run (on
large input sizes)
¤ We don’t care about constant factors (we care about how
many iterations we make, not how many operations we have
to do in each iteration)
What Do We Gain?
¤Show important characteristics in terms of resource
requirements
¤Suppress tedious details
¤Matches the outcomes in practice quite well
¤As long as operations are faster than some constant
(1 ns? 1 μs? 1 year?), it does not matter
Linear Search: Best Case
Simplified
# let n = the length of list.
def search(list, key):
index = 0
while index < len(list): 1 iteration
if list[index] == key:
return index
index = index + 1
return None
Linear Search: Worst Case
Simplified
# let n = the length of list.
def search(list, key):
index = 0
while index < len(list): n iterations
if list[index] == key:
return index
index = index + 1
return None
Order of Complexity
¤ For very large n, we express the number of operations
as the (time) order of complexity.
¤ For asymptotic upper bound, order of complexity is
often expressed using Big-O notation:
¤ Number of operations Order of Complexity
¤ n O(n)
Usually doesn't
¤ 3n+3 O(n) matter what the
constants are...
¤ 2n+8 O(n) we are only
concerned about
the highest power
of n.
Why don’t constants matter?

(n=1) 45n3 + 20n2 + 19 = 84

(n=2) 45n3 + 20n2 + 19 = 459

(n=3) 45n3 + 20n2 + 19 = 1414


O(n) (“Linear”)
2n + 8
3n+3 n

Number of
Operations

n
(amount of data)
O(n)
Number of
n
Operations
30 For a linear algorithm,
if you double the amount
of data, the amount of work
you do doubles
20 (approximately).

10

10 20 30 n
(amount of data)
O(1) (“Constant-Time”)
Number of For a constant-time algorithm,
Operations if you double the amount
of data, the amount of work
you do stays the same.

4 = O(1)
4

1 = O(1)
1

n
(amount of data)
Linear Search
¤Best Case: O(1)

¤Worst Case: O(n)

¤Average Case: ?
¤ Depends on the distribution of queries
¤ But can’t be worse than O(n)
Insertion Sort
Insertion Sort

# let n = the length of list.


def isort(list):
i = 1
while i != len(list): n-1 iterations
move_left(list, i)
i = i + 1
return list
move_left
# let n = the length of list.
def move_left(a, i):
x = a.pop(i)
j = i - 1
while j >= 0 and a[j] > x:
j = j – 1
a.insert(j + 1, x)
move_left
# let n = the length of list.
def move_left(a, i): at most
x = a.pop(i)
j = i - 1
while j >= 0 and a[j] > x: i iterations
j = j – 1
a.insert(j + 1, x)

but how long do pop and insert take?


Measuring pop and insert
2 million elements in list, 1000 inserts: 0.7548720836639404 seconds

4 million elements in list, 1000 inserts: 1.6343820095062256 seconds

8 million elements in list, 1000 inserts: 3.327040195465088 seconds

8 million elements in list, 1000 pops: 2.031071901321411 seconds

16 million elements in list, 1000 pops: 4.033380031585693 seconds

32 million elements in list, 1000 pops: 8.06456995010376 seconds

Doubling the size of the list doubles the cost (time) of insert or pop. These functions
take linear time.
Insertion Sort: cost of move left
# let n = the length of list.
def move_left(a, i):
x = a.pop(i) n iterations
j = i - 1
while j >= 0 and a[j] > x: i iterations
j = j – 1
a.insert(j + 1, x) n iterations

Total cost (at most): n + i + n


But what is i? To find out, look at isort, which calls
move_left, supplying a value for i
Insertion Sort: what is the cost of
the whole thing?
# let n = the length of list.
def isort(list):
i = 1
while i != len(list): #n-1 iterations
move_left(list,i) #i goes from 1 to n-1
i = i + 1
return list
Total cost: cost of move_left as i goes from 1 to n-1

Cost of all the move_lefts: n + 1 + n


+n+ 2 + n
+n+ 3 + n
...
+n+ n-1 + n
In place iSort Worst Case…

j i

¤ • On iteration i, we need to examine j elements and then


shift i-j elements to the right, so we have to do j + (i-j) = i
units of work.
Remember…

¤ What are we trying to do?


¤ Understand the cost of insertion sort
¤ How do we understand that cost?
¤ Via order of complexity – finding the highest order term
¤ What does this require?
¤ 1st generalizing the cost as an equation
¤ Then simplifying the equation to find highest order term
Figuring out the sum
¤ n+1+n (n-1)*2n
¤ +n+2+n +1
¤ +n+3+n
+2
¤ ...
+3
¤ + n + n-1 + n
...
+ n-1
Adding 1 through n-1
1 2 3 4 5 6 7

2
(6 * 7) / 2
3 blue circles

6
Adding 1 through n-1
¤ We saw 1 + 2 + ... + 6 = (6 * 7) / 2

¤ Generalizing, 1 + 2 + ... + n-1 = (n-1)(n) / 2

¤ So our whole cost is:


¤ (n-1)*2n + 1 + 2 + 3 ... + n-1
¤ = (n-1)*2n + (n-1)(n) / 2
¤ = 2n2 - 2n + (n2 - n) / 2
¤ = (5n2 - 5n) / 2 = (5/2)n2 - (5/2)n
¤ Observe that the highest-order term is n2
A different way…

¤ When i=1,we have1 unit of work.


¤ When i=2, we have 2 units of work.

¤ ...
¤ When i = n-1, we have n-1 units of work.
¤ The total amount of work done is:

¤ 1 + 2 + ... + (n-1)
= n(n-1)/2
= (n2 - n)/2 (a quadratic function)
= O(n2)
Let’s look at this again…
slowly
Examining the cost
def isort(list):
i = 1
while i != len(list):
move_left(list,i)
i = i + 1
return list
Examining the cost
def isort(list):
i = 1
while i != len(list):
move_left(list,i)
i = i + 1
return list
Examining the cost
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
i = i + 1
return list
Examining the cost
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
pop
while loop
insert
i = i + 1
return list
Again…
2 million elements in list, 1000 inserts: 0.7548720836639404 seconds

4 million elements in list, 1000 inserts: 1.6343820095062256 seconds

8 million elements in list, 1000 inserts: 3.327040195465088 seconds

8 million elements in list, 1000 pops: 2.031071901321411 seconds

16 million elements in list, 1000 pops: 4.033380031585693 seconds

32 million elements in list, 1000 pops: 8.06456995010376 seconds

Doubling the size of the list doubles the cost (time) of insert or pop. These functions
take linear time.
Examining the cost
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
pop…………………..…….. n
while loop
insert
i = i + 1
return list
Examining the cost
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
pop…………………..…….. n
while loop
insert …………………..…. n
i = i + 1
return list
Examining the cost
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
pop & insert ......... n + n
while loop

i = i + 1
return list
Examining the cost
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
pop & insert ......... 2n
while loop

i = i + 1
return list
Examining the cost
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
pop & insert ......... 2n
while loop 1+

i = i + 1
return list
Examining the cost
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
pop & insert ......... 2n
while loop 1+2+

i = i + 1
return list
Examining the cost
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
pop & insert ......... 2n
while loop 1+2+3…

i = i + 1
return list
How can we express this?
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
pop & insert ......... 2n
while loop 1+2+3…n-1

i = i + 1
return list
How can we express this?
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
pop & insert ......... 2n
while loop 1+2+3…n-1

i = i + 1
return list

1+2+3…n-1
Test for n = 7

1+2+3…n-1
Test for n = 7.

1+2+3+4+5+6
1+2+3…n-1
Test for n = 7.

1+2+3+4+5+6
1+2+3…n-1
Test for n = 7.

(6) * (7) / 2 blue circles

1+2+3+4+5+6
1+2+3…n-1
Test for n = 7.

(6) * (7) / 2 blue circles


(n-1) * (n) / 2 blue circles

1+2+3+4+5+6
1+2+3…n-1
Our equation …

(6) * (7) / 2 blue circles


(n-1) * (n) / 2 blue circles

(n-1)*n/2
1+2+3…n-1
Our equation …

(n-1)*n/2
1+2+3…n-1
How can we express this?
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
pop & insert ......... 2n
while loop 1+2+3…n-1

(n-1)*n/2i = i + 1
return list

1+2+3…n-1
How can we express this?
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
pop & insert ......... 2n
while loop 1+2+3…n-1

i = i + 1
return list

(n-1)*n/2
Combine to calculate
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
pop & insert ......... 2n +
while loop (n-1)*n/2

i = i + 1
return list
How can we express this?
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i)
pop & insert & while 2n + (n-1)*n/2

i = i + 1
return list
How can we express this?
def isort(list):
i = 1
while i != len(list): n-1
move_left(list,i) 2n + (n-1)*n/2

i = i + 1
return list
Total number of operations
def isort(list):
i = 1
while i != len(list): (n-1) * 2n + (n-1)*n/2
move_left(list,i)

i = i + 1
return list
Generalizing…

(n-1) * 2n + (n-1)*n/2

¤ = 2n2 - 2n + (n2 - n) / 2
¤ = (5n2 - 5n) / 2
¤ = (5/2)n2 - (5/2)n
Highest order term? …

(5/2)n2 – (5/2)n

n2
Order of Complexity

Number of operations Order of Complexity


n2 O(n2)
(5/2)n2 - (1/2)n O(n2)
2n2 + 7 O(n2)
Usually doesn’t matter what
the constants are… f(n) is O(g(n)) means
we are only concerned about
the highest power of n.
f(n) < g(n)・k for some
positive k
Keep It Simple
¤ “Big O” notation expresses an upper bound:
f(n) is O(g(n)) means f(n) < g(n)・k
(whenever n is large enough)
¤ So if f(x) is O(n2), then f(x) is O(n3) too!
¤ But we always use the smallest possible function, and the
simplest possible.
¤ We say 3n2 + 4n + 1 is O(n2), not O(n3)
¤ We say 3n2 + 4n + 1 is O(n2), not O(3n2 + 4n)
¤ ...even though all of the above are true
O(n2) (“Quadratic”)
n2
2n2 + 7 n2/2 + 3n/2 – 1

Number of
Operations

n
(amount of data)
O(n2)
Number of
N2
Operations
For a quadratic algorithm,
900 if you double the amount
of data, the amount of work
you do quadruples
(approximately).

400

100

10 20 30 N
(amount of data)
Insertion Sort
¤ Worst Case: O(n2)

¤ Best Case: ?

¤ Average Case: ?

¤ We’ll compare these algorithms with others soon to see how


scalable they really are based on their order of complexities.
Big O

¤ O(1) constant

¤ O(log n) logarithmic

¤ O(n) linear

¤ O(n log n) log linear

¤ O(n2) quadratic

¤ O(n3) cubic

¤ O(2n) exponential

You might also like