OOP w08 Publish

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 51

STL LIBRARY

OBJECT ORIENTED PROGRAMMING


Advisor: Trương Toàn Thịnh

1
CONTENTS
Introduction
Class Stack
Class Queue
Class Vector
Class List
Iterator design pattern
General algorithms

2
INTRODUCTION
Abstracting data help us focus on our
Abstract-Data-Type-based problems, and
not care about the object’s inside details
Containers are the critical classes
showing abstract datatype
Main goal of abstract datatypes separates
them from applications
Abstract datatypes are flexible, guarantee
their type-safe and performance
3
INTRODUCTION (CONTAINER)
Below figure show abstract objects and
their relationship

to f>>
Container
men
< < el e
// File: Object.h
#ifndef _OBJECT_H_
#define _OBJECT_H_
class Object { Stack Queue PriorityQueue Tree
public: virtual ~Object() = 0;
};
#endif
Dequeue BinaryTree

4
INTRODUCTION (CONTAINER)
Below figure show abstract objects and
their relationship
// File: Container.h
#ifndef _CONTAINER_H_ // File: Object.h
#define _CONTAINER_H_ #ifndef _OBJECT_H_
#define “Object.h” #define _OBJECT_H_
class Container { class Object {
public: public: virtual ~Object() = 0;
virtual void put(Object&) = 0; };
virtual Object& get() = 0; #endif
removeable

<<element o
virtual Object& peek() = 0;
virtual bool isEmpty() const { if(iCount == 0) return true; return false; }
virtual bool isFull() const {

> f>
if(iCount == MAX_ELEMENTS) return true; return false;
}
virtual int numberOfElems() const { return iCount; }
private: int iCount; // A number of elements of Container
Container
};
#endif 5
INTRODUCTION Stack

A data structure follows Last-In-First-Out


(LIFO) mechanism
Two basic operations:
◦ Push: put a member at the top of the stack
◦ Pop: remove a member from the top of the
stack

Source: https://en.wikipedia.org/wiki/Stack_(abstract_data_type) 6
INTRODUCTION Stack

A data structure follows Last-In-First-Out


(LIFO) mechanism
// File: Stack.h // File: Stack.cpp Object& Stack::pop() {
#ifndef _STACK_H_ #include “Stack.h” if(!isEmpty()) {
#define _STACK_H_ void Stack::push(Object& obj) --iCount;
#include “Container.h” { return listObjs.extractHead();
class Stack: public Container { if(!isFull()) { }
public: listObjs.addFirst(obj); return DUMMY;
void push(Object&); ++iCount; }
Object& pop(); }
Object& top(); }
private: Object& Stack::top() {
LinkedList<Object> listObjs; if(!isEmpty()) return listObjs.getHead();
}; return DUMMY;
#endif }

7
INTRODUCTION Queue

A data structure follows First-In-First-Out


(FIFO) mechanism
Two basic operations:
◦ Enqueue: put a member at the end of the
queue
◦ Dequeue: remove a member from the top of
the queue

Source: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics) 8
INTRODUCTION Queue

A data structure follows First-In-First-Out


(FIFO) mechanism
// File: Queue.h // File: Queue.cpp Object& Queue::dequeue() {
#ifndef _QUEUE_H_ #include “Queue.h” if(!isEmpty()) {
#define _QUEUE_H_ void Queue::enqueue(Object& obj) --iCount;
#include “Container.h” { return listObjs.extractHead();
class Queue: public Container { if(!isFull()) { }
public: listObjs.addLast(obj); return DUMMY;
void enqueue(Object&); ++iCount; }
Object& dequeue(); }
Object& first(); }
private: Object& Queue::first() {
LinkedList<Object> listObjs; if(!isEmpty()) return listObjs.getHead();
}; return DUMMY;
#endif }

9
INTRODUCTION PriorityQueue

 Three basic operations:


◦ Enqueue: put a member into the priority-queue
◦ Dequeue: remove a member with highest priority
◦ First: give information of a member with highest
priority
// File: PriorityQueue.h highest priority
X1
#ifndef _PRIORITYQUEUE_H_
#define _PRIORITYQUEUE_H_
#include “Container.h” X2 X3
class PriorityQueue: public Container {
public:
void enqueue(Object&);
X4 X5 X6 X7
Object& dequeue();
Object& first();
private:
Heap<Object> listObjs;
}; using AVL or one-dimensional array
#endif
10
INTRODUCTION PriorityQueue

A data structure follows First-In-First-Out


(FIFO) mechanism
// File: PriorityQueue.cpp Object& PriorityQueue::dequeue() {
#include “PriorityQueue.h” if(!isEmpty()) {
void PriorityQueue::enqueue(Object& obj) --iCount;
{ return listObjs.extractRoot();
if(!isFull()) { }
listObjs.add(obj); return DUMMY;
++iCount; }
}
}
Object& PriorityQueue::first() {
if(!isEmpty()) return listObjs.getRoot();
return DUMMY;
}
11
INTRODUCTION (DISCUSS)
Stack,Queue and PriorityQueue have three
methods
◦ Put a member into the structure
◦ Get a member from the structure
◦ View a member being taken from the structure
Depending on the type of structures, the
way and the names of these methods are
different
◦ In stack: push, pop and top
◦ In Queue/PriorityQueue: enqueue, dequeue and
first 12
INTRODUCTION (SOLUTION)
Building a Container with pure virtual
methods: put, get and peek
Then, Stack and Queue inherit Container
and override these three methods
Container

+put(in obj: object) : void = 0


+get() : object = 0
+peek() : object = 0

Stack Queue

+put(in obj: object) : void +put(in obj: object) : void


+get() : object +get() : object
+peek() : object +peek() : object
13
INTRODUCTION (COMMENT)
This solution is good at dealing with the
problem of being unknow to which class
chosen at compile time
Container* pStructure;
// later can do as below
pStructure = new Stack();
However, the common names of Stack are
push, pop and top, while Queue has
enqueue, dequeue and first. We can do

void Stack::push(Object& obj) {put(obj);}


Object& Stack::pop() {return get();}
Object& Stack::top() {return peek();}
This method has a drawback that now “Stack” class has six methods 14
INTRODUCTION
(STL Library)
Standard library of C++ is the foundation
of programming language C++
Common class library of C++ (called
Standard Template Library – STL) is a
important component of standard library
STL library has main parts
◦ Container: the classes with set form
◦ Iterator: the class standing for enumeration of
the members of Container
◦ Algorithm: The methods to process the
members of Container 15
INTRODUCTION
(STL Library)
 The class Container can be divided into
◦ Sequence Containers: the classes are vector, deque, list.
And the members are maintained the orders the same as they
are added
◦ Associative Containers: the classes are set, map, hash_map,
hash_set, multimap, multiset, hash_multimap, hash_multiset.
And the members are saved with the predefined order.
Associative containers are divided into two sub-categories
 Set: includes set, hash_set, multiset and hash_multiset. The members
have no key field.
 Map: includes map, hash_map, multimap and hash_multimap. The
members have key and value fields
◦ Container Adapters: the classes are priority_queue, queue,
stack. Actually, they are the classes covering the classes
belonging to the first category
16
INTRODUCTION
Figure
Queue
Algorithm Container
Standard Stack
Adapters
Template Iterator Priority
Library Sequence queue
Containers
Containers
vector
Associative deque
Containers
list
set map

multiset multimap

hash_set hash_map

hash_multiset hash_multimap 17
CONTENTS
Introduction
Class Stack
Class Queue
Class Vector
Class List
Iterator design pattern
General algorithms

18
CLASS STACK
Include the library <stack>
template <class Type, class Container = deque<Type>>
class stack{}
Where:
◦ Type: datatype of members the stack contains
◦ Container: main data structure of stack
Figure:
empty() Check if empty or not
push pop
pop() Remove a member
top() Get a member
push() Add a member
top
size() Get the size of stack
19
CLASS STACK
The 1st example
#include <iostream> #include <fstream> #include <stack>
#include <list> #include <vector> #include <string>
using namespace std;
typedef stack<string> Stack1; // Default execution class
typedef stack<string, vector<string>> Stack2; // vector<string> class
typedef stack<string, list<string>> Stack3; // list<string> class
int main(int argc, char* argv[]) {
requireArgs(argc, 1); ifstream f(argv[1]); assure(f, argv[1]);
Stack1 textlines; string line;
while(getline(f, line)) textlines.push(line + “\n”);
while(!textlines.empty()){
cout << textlines.top();
textlines.pop();
}
return 0;
}
20
CLASS STACK
The 2nd example
template <class Stk> void stackOut(Stk& s, ostream& os = cout) {
while(!s.empty()) {
int main(int argc, char* argv[]) {
os << s.top() << “\n”; s.pop();
requireArgs(argc, 1);
}
ifstream f(argv[1]);
}
assure(f, argv[1]);
class Line { list<Line> lines;
string line; int lspaces;
string s;
public:
while(getline(f, s)) lines.push_front(s);
Line(string s) : line(s) {
stack<Line, list<Line>> stk(lines);
lspaces = line.find_first_not_of(‘ ’);
stackOut(stk);
if(lspaces == string::npos) lspaces = 0;
return 0;
line = line.substr(lspaces);
}
}
friend ostream& operator<<(ostream& os, const Line& L) {
for(int i = 0; i < L.lspaces; i++) os << ‘ ’;
return os << L.line;
}
// other methods…
}; 21
CLASS QUEUE
Include the library <queue>
template <class Type, class Container = deque<Type>>
class queue{}
Where:
◦ Type: datatype of members the queue contains
◦ Container: the main data structure of queue
Figure: empty() Check if empty or not
pop() Remove a member
push top() Get a member
push() Add a member
Tail
size() Get the size of stack
front back() Get the member at the end of queue
Head pop front() Get the member at the front of the queue
22
CLASS QUEUE
Example
#include <iostream> Queue q:
#include <queue> 123
#include <string> Queue q:
using namespace std; 2345
void printQueue(queue<int> q) { Current size of queue: 4
cout << “Queue q: ” << endl;
while(!q.empty()) { cout << q.front() << “ ”; q.pop() }
cout << endl;
}
void main() {
queue<int> q;
q.push(1); q.push(2); q.push(3); printQueue(q);
q.pop();
q.push(4); q.push(5); printQueue(q);
cout << “Current size of queue: ” << q.size();
}
23
CLASS VECTOR
Store the members with adjacent order as
in array
The cost of adding or removing is high
The cost of random access is low
Easily Extending the size is easier than
dynamic array
pop_back
Figure:
01 2 3 4 5 6 7

back
front push_back
24
CLASS VECTOR
 Some important methods
◦ clear(): delete all members
◦ empty(): check if empty or not
◦ erase(): delete one or an array of members
◦ front(), back(): return the reference of the first and the last member
◦ insert(): add a member at another position
◦ pop_back(), push_back(): delete and add a member at the end of vector
◦ size(): return the size of vector
◦ reserve(): allocate a minimum memory for vector
◦ resize(): re-size the vector
 Example
#include <iostream> 123456
#include <vector>
using namespace std;
void main() {
vector<int> coll;
for(int i = 1; i <= 6; i++) coll.push_back(i);
for(int i = 0; i < coll.size(); i++) cout << coll[i] << ‘ ’;
cout << endl;
} 25
CLASS LIST
Store the members with order of double
linked list
Adding or removing the member is quick
Random access is slow
Figure:

pop_back pop_front

push_back
push_front

26
CLASS LIST
 Some important methods
◦ clear(): delete all members
◦ empty(): check if empty or not
◦ erase(): delete one or an array of members
◦ front(), back(): return the reference of the first and the last member
◦ insert(): add a member at another position
◦ pop_front(), push_front(): delete and add a member at the start of
vector
◦ pop_back(), push_back(): delete and add a member at the end of vector
◦ size(): return the size of vector
◦ merge(): join another list with current list and sort with another order
(two lists must be sorted before)
◦ splice(): join a part of another list with current list
◦ reserve(): allocate a minimum memory for vector
◦ unique(): eliminate the member with the same value
27
CLASS LIST
Example List 1: 0 1 2 3 4 5
List 2: 5 4 3 2 1 0
#include <iostream>
List 1:
#include <list>
using namespace std;
List 2: 5 4 0 1 2 3 4 5 3 2 1 0
void printList(ostream& os, list<int> &L1) { List 1: 0 0 1 1 2 2 3 3 4 4 5 5
list<int>::iterator pos = L1.begin(); List 2: 0 1 2 3 4 5
while(pos != L1.end()) {os << (*pos) << “ ”; ++pos;} List 1: 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 5 5 5
os << endl; List 2:
}
void printTwoLists(ostream& os, list<int> &L1, list<int> &L2) {
os << “List 1: ”; printList(os, L1); os << “List 2: ”; printList(os, L2);
}
void main() {
list<int> l1, l2;
for(int i = 0; i < 6; i++){l1.push_back(i); l2.push_front(i);}
printTwoLists(cout, l1, l2);
l2.splice(find(l2.begin(), l2.end(), 3), l1);
printTwoLists(cout, l1, l2);
l2.sort(); l1 = l2; l2.unique();
printTwoLists(cout, l1, l2);
l1.merge(l2);
printTwoLists(cout, l1, l2);
} 28
OTHER CLASSES
 “set” belongs to associative container
◦ It has no repeated member
◦ Members in “set” are sorted & saved in red-black tree
◦ Cannot directly edit a member. Need 2 steps
 Delete the member of old value
 Add a member with new value
 “multiset” is similar to “set”, but allow repeated
value
 “hash_set” and “hash_multiset” are similar to “set”
and “multiset”, but saved in hash-table
 “set” and “multiset” belongs to <set>, and
“hash_set” and “hash_multiset” belongs to
<hash_set> 29
OTHER CLASSES
Example
#include <iostream> 654321
#include <set>
using namespace std;
template <class T>
void printSet(ostream& os, T& ms) {
typename T::iterator pos = ms.begin();
while(pos != ms.end()) {os << (*pos) << “ ”; ++pos;}
os << endl;
need a space
}
void main() {
set<int, greater<int> > ms;
int a[] = {2, 6, 4, 1, 5, 3}, n = sizeof(a)/sizeof(a[0]);
for(int i = 0; i < n; i++){
ms.insert(a[i]);
}
printSet(cout, ms);
} 30
OTHER CLASSES
“map” is similar to “set”, but a member in
“map” includes two parts, key and value.
And key-part must be unique
“multimap” is similar to “map”, but allow
key-part has the same value
Map Multimap

2 x 5 y 1 x 3 y

1 z 4 y 1 z 3 y

3 y 6 z 2 y 3 z

31
OTHER CLASSES
Example
#include <iostream> (1, This)(2, is)(3, an)(4, example)(5, of)(6, map)
#include <map> #include <string>
using namespace std;
template <class T> void printMap(ostream& os, T& m) {
typename T::iterator pos = m.begin();
while(pos != m.end()) {
os << “(” << pos->first << “, ” << pos->second << “)”; ++pos;
}
os << endl;
}
void main() {
map<int, string> m1; typedef pair<int, string> MyPair;
m1.insert(MyPair(2, “is”)); m1.insert(MyPair(4, “example”));
m1.insert(MyPair(3, “an”)); m1.insert(MyPair(1, “This”));
m1.insert(MyPair(6, “map”)); m1.insert(MyPair(5, “of”));
printMap(cout, m1);
} 32
ITERATOR DESIGN PATTERN
Separating the enumeration out of the set.
There are many ways of enumeration
Advantages:
◦ Can enumerate with many ways in the same
set.
◦ Simplify the class of set (because the
enumeration is separated)
◦ A set can choose many ways of enumerations

33
ITERATOR DESIGN PATTERN
Figure:
Iterator
Aggregate Client

+First()
+CreateIterator() +Next()
+IsDone()
+CurrentItem()

ConcreteAggregate
ConcreteIterator
+CreateIterator()

return new ConcreteIterator(this)

34
ITERATOR DESIGN PATTERN
The iterator used to enumerate in the
container is declared as follows
◦ Container_name<parameter>::iterator name
◦ Container_name<parameter>::const_iterator
name
The containers provide two basic methods
◦ begin(): return the iterator object pointing to the
first member of a set
◦ end(): return the iterator object pointing to the
member behind the last member of a set
35
ITERATOR DESIGN PATTERN
 Class Iterator provides some operators:
◦ Operator *: get the value at the element (address) the iterator
object is pointing to
◦ Operator ++: the iterator object points to the next element
◦ Operator == and !=: compare two values of two iterator
objects (Consider if these two objects point to the same
address)
◦ Assignment operator ‘=’
 Iterator may be classified as follows:
◦ 2-way scanning: iterator of list, set, hash_set, hash_multiset,
multiset, map, hash_map, multimap, hash_multimap (using 2
operators: ++ and --)
◦ Random access: iterator of vector and deque (using 4
operators: +=, -=, > and <) 36
ITERATOR DESIGN PATTERN
Example
#include <iostream>
#include <set> 4
using namespace std;
void main()
{
2 6
set<int> c;
int data[] = {3, 1, 5, 4, 1, 6, 2};
int n = sizeof(data) / sizeof(data[0]);
for(int i = 0; i < n; i++){
c.insert(data[i]); 1 3 5
}
set<int>::const_iterator p = c.begin();
while(p != c.end()){
cout << *p << “ ”;
++p; Enumerate with default order of a set (LNR):
} 123456
}

37
GENERAL ALGORITHMS
STL library implements some algorithms
processing the elements of Container
◦ Sort
◦ Copy
◦ Sum…
All algorithms are declared in <algorithm>
and <numeric>
◦ <numeric>: provides the methods of numeric
manipulation, such as addition or multiplication
◦ <algorithm>: provides the methods of set
manipulation, such as sort or search…
38
GENERAL ALGORITHMS
Example
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void main() {
vector<int> c; vector<int>::iterator p;
int data[] = {2, 1, 5, 4, 3, 6}, n = sizeof(data)/sizeof(data[0]);
for(int i = 0; i < n; i++) c.push_back(data[i]);
p = min_element(c.begin(), c.end());
cout << “min: ” << *p << endl;
p = max_element(c.begin(), c.end());
cout << “max: ” << *p << endl;
p = find(c.begin(), c.end(), 3);
reverse(p, coll.end());
p = coll.begin();
while(p != coll.end()){ cout << *p << ‘ ’; ++p; }
} 39
GENERAL ALGORITHMS
The algorithms interacting many sub-
array simultaneously
◦ copy(): copy the value of an array of members
to new array
◦ equal(): check if two arrays are equal or not
Example

void main(){
list<int> c1; vector<int> c2;
for(int i = 0; i < 10; i++) c1.push_back(i);
c2.resize(c1.size());
copy(c1.begin(), c1.end(), c2.begin());
}
40
COMPLEX NUMBER
STL provides template complex with
methods, operator in <complex> header
Remind:
◦ Complex x = a + bi with i2 = -1, where a is real
part and b is imaginary part
◦ Alternative form of x = r(cos + isin), where r
is modulus part and  is argument part
◦ We have r = and  = arctan(b/a)
◦ If x = a + bi, then = a – bi is a x’s complex
conjugate
◦ Norm of x = a + bi is a2 + b2 (norm(x) = a2 + b2)
41
COMPLEX NUMBER
 Some methods:
◦ polar(): return complex number corresponding to modulus and
argument
◦ abs(): return a complex number’s modulus
◦ arg(): return a complex number’s argument
◦ conj(): return a complex conjugate
◦ imag(): return a complex’s imaginary part
◦ real(): return a complex’s real part
◦ norm(): return a norm of a complex number
◦ cos(), sin(): return a value of cos/sin function of complex number
◦ exp(): return a value of e-based exponential function of complex
number
◦ operator: addition (+, +=), subtraction (-, -=), multiplication (*,
*=), division (/, /=), assignment (=), comparison (==, !=), insertion
(<<) and extraction (>>)
42
COMPLEX NUMBER
Example:
#include <iostream> Create c1 from real and imaginary: c1 = (4,5)
#include <complex> Create c2 from c1 : c2 = (4,5)
using namespace std; c3 = polar(sqrt(8), pi / 4) = (2,2)
Modulus of c3 : abs(c3) = 2.82843
void main() {
Argument of c3 :
double pi = 3.14159265359; arg(c3) = 0.785398 radians(= 45 do).
complex<double> c1(4.0, 5.0);
cout << “Create c1 from real and imaginary: ” << “c1 = ” << c1 << endl;
complex<double> c2(c1);
cout << “Create c2 from c1: ” << “c2 = ” << c2 << endl;
complex<double> c3(polar(sqrt((double)8), pi/4));
cout << “c3 = polar(sqrt(8), pi/4) = ” << c3 << endl;
double absc3 = abs(c3);
double argc3 = arg(c3);
cout << “Modulus of c3: abs(c3) = ” << absc3 << endl;
cout << “Argument of c3:\n arg(c3) = ” << argc3 << “ radians (= ” << argc3*180/pi << “ do).” << endl;
}

43
COMPLEX NUMBER
Type conversion
◦ Also, three types including complex<float>,
complex<double> and complex<long double>
follow implicit and explicit type-casting
◦ Example:
 complex<float> cf(1.0, 2.0);
 complex<long double> cld(4.0, 5.0);
 complex<double> cd1 = cf; // OK
 complex<double> cd2 = cld; // Error
◦ Need to explicit casting
 complex<double> cd2 = (complex<double>)cld; //OK
44
COMPLEX NUMBER
Comparison operator
◦ Only support “==” and “!=”, not “>” or “<”
◦ Comparing a complex with a float makes a float
to be a complex
#include <iostream>
#include <complex>
using namespace std;
void main() {
complex<double> cd(1, 0);
double d1 = 1.0;
if(d1 == cd) cout << “cd = d1” << endl;
else cout << “cd != d1” << endl;
} 45
COMPLEX NUMBER
Insertion (<<) and extraction (>>)
operators
◦ Insertion << operator follows (real, imaginary)
◦ Extraction >> operator follows (real <space>
imaginary)
#include <iostream>
#include <complex>
using namespace std;
void main() {
complex<double> cd;
cin >> cd;
cout << “So phuc da nhap: cd = ” << cd << endl;
} 46
EXERCISE (EXPRESSION)
A mathematical expression includes terms
being positive whole numbers and
operators, such as +, -, *, / or parentheses ()
Evaluate an expression and print the result
Example: 4 * (7 + 3) / 5 + 6  result = 14
Hint:
◦ Convert the expression to “reverse Polish
notation” and push it to stack
◦ Using stack to evaluate
Example: 473+*5/6+ 47
EXERCISE (EXPRESSION)
Algorithm of “reverse Polish notation”
while(expression still Has member) 4  (7 + 3) / 5 + 6
Read this member & Remove it from expression
if(this member is term) Enqueue it into result-queue
else if(this member is operator) // for example o1
while(operator o2 is top of stack)
6
if(o2 has More priority than o1)
Pop o2 from stack and Enqueue it into result-queue
5
end-while
Push o1 into stack
else if (this member is ‘(’) Push it into stack
else if(this member is ‘)’) 3 +
while(the top of stack != ‘(’)
7 (
Pop it and Enqueue into result-queue
end-while 4 +/

Pop ‘(’ from stack
end-while 4 7 3 +5 / 6 +
while(stack is not Empty)
Pop each member and Enqueue into result-queue
48
end-while
EXERCISE (EXPRESSION)
Algorithm of evaluate an expression with
“reverse Polish notation” form
while(expression still Has member) 473+5/6+
Read this member & Remove it from expression
if(this member is term) Push it into stack
else // is operator, assume we need n terms
if(a number of terms in stack < n) error “not enough terms”
else
Pop n members from stack
Compute this math
Push result of this math into stack
end-while
if (there is one member in stack) print the final result 3
else // There are more than one, so user input wrong expression
657
10
error “Error expression”
14
48
40

49
EXERCISE (GRAPH)
Let G = (V, E), where V is a collection
of vertices and E is a collection of edges
◦ V = {1, 2, …n} and E = {(i1, j1), …, (im, jm)}
◦ |V| = n vertices and |E| = m edges
◦ ik  [1, n] and k  [1, m]
Randomly input a starting vertex X and
print the list of vertices “breadth-first
search” from X
Hint: using queue

50
EXERCISE (GRAPH)
Pseudo code 1
Initialize an array Mark[n] = {0}
2
Choose X: enqueue X into queue
while(Queue is not empty) 3
dequeue to a variable called y
Mark[y] = 1; 4
Print y 5
for all z adjacent to y & Mark[z] = 0 9
enqueue z
end-for 6
7
end-while

8
7 8
2 4
6
9 5 13

V = {1, 2, 3, 4, 5, 6, 7, 8, 9}
E = {(1, 3), (1, 4), (2, 4), (2, 9), (3, 5), (3, 6), (4, 5), (4, 7), (6, 8), (7, 8), (8, 9)} 51

You might also like