Algorytm tego sortowania wykorzystuje modelowanie struktury drzewiastej w typie tablicowym.
1. Umieszczamy w tablicy jako element pierwszy korzeń drzewa binarnego (klucz korzenia).
2. Dalej, dla element udrzewa binarnego, który jest elementem tablicy o indeksie i lewy element umieszczamy na miejscu o indeksie 2i, a prawy na miejscu o indeksie 2i+1.
Tablica reprezentuje wtedy drzewo binarne
Przypomnienie:
Drzewo binarne nazywamy kopcem, gdy dla dowolnego elementu pozostałe elementy poddrzewa, dla którego jest on korzeniem, są od niego
mniejsze.
Uporządkujmy zbiór n-elementowy, umieszczając jego elementy w tablicy, nadając jej strukturę kopca.
A[1] jest elementem największym.
Jeśli A[1] zamienimy z A[n] i w tablicy z pominiętym elementem A[n]
(n-1 –elementowej), przywrócimy strukturę kopca, to A[1] ponownie będzie największy, tym razem spośród A[1], A[2], …, A[n-1].
Zamieniamy ponownie A[1] z (tym razem) A[n-1], otrzymujemy sytuację wyjściową dla tablicy o n-2 elementach.
Powtarzając to postępowanie, otrzymamy tablicę 1-elementową (najmniejszy element kopca).
Otrzymana tablica stanowi zbiór uporządkowany – sortowanie przez kopcowanie
Dwie funkcje
zamien – heapsort
Zalety:
•Rząd pesymistycznej złożoności obliczeniowej jest równy nlog2n, jest to mniejsza złożoność niż dla prostych algorytmów sortowania.
•Algorytm jest optymalny pod tym względem.
Wady:
•Brak wrażliwości na wstępne uporządkowanie tablicy (średnia złożoność jest tego samego rzędu, co pesymistyczna).
Przykład:
Rozważmy zbiór liczb całkowitych S={a1, a2, …, an} oraz algorytm sortujący G bazujący na operacji porównania jako jedynej informacji o elementach zbioru decydującej o porządku jego elementów.
Drzewem decyzyjnym związanym ze zbiorem S i algorytmem G nazywamy drzewo binarne, którego elementy zawierają:
-pary elementów z S podlegające kolejnym porównaniom podczas sortowania algorytmem G,
- Wskazania na lewy i prawy element w zależności od wartości porównania.
Drzewo decyzyjne dla zbioru trzyelementowego.
Strzałki w lewo – odpowiedź TAK.
Strzałki w prawo – odpowiedź NIE.
Prostokąty –liście drzewa decyzyjnego.
Każdy algorytm sortujący przez porównania zbiór n-elementowy można przedstawić w postaci drzewa decyzyjnego o n! liściach zawierających wszystkie permutacje elementów zbioru S.
Elementy wewnętrzne mają zawsze dwa następniki.
Długości ścieżek od korzenia do liścia są równe ilości porównań w trakcie porządkowania.
Oszacowanie z dołu pesymistycznej złożoności obliczeniowej
wysokość drzewa decyzyjnego h(G)=
2
h(G)n!
Dowód: Indukcja względem
wysokości drzewa
h(G) log2(n!) =
Jest to oszacowanie z dołu liczby porównań.
Można pokazać, że to oszacowanie pesymistycznej złożoności jest również oszacowaniem z dołu średniej złożoności obliczeniowej.
Pytanie:
Czy uwzględniając inne dodatkowe własności elementów zbioru można otrzymać algorytmy o mniejszym od nlog2n rzędzie zbieżności?
TAK
Sortowanie przez zliczanie
Założenia:
• Zbiór S jest n-elementowy.
• Zbiór S zawiera liczby naturalne z przedziału 1,k, gdzie nk
S={a1, a2, …, an}, dla i=1,2,…,n jest 1 ai k Na czym polega?
• Dla elementu ai wyznaczamy liczbę j określającą ilość elementów z S mniejszych od ai .
• Element ai ustawiamy na miejscu j+1-szym.
• Jeśli takich elementów jest więcej (mających j elementów w S od nich mniejszych), to ustawiamy je na miejscach kolejnychj+2, j+3 i dalszych.
• Postępujemy tak z każdym elementem zbioru S.
Jeśli k jest rzędu O(n), to algorytm sortowania przez zliczanie ma pesymistyczną złożoność obliczeniową rzędu O(n).
Podstawowe algorytmy numeryczne:
• poszukiwanie miejsc zerowych funkcji;
• iteracyjne obliczanie wartości funkcji;
• interpolacja funkcji metodą Lagrange’a;
• różniczkowanie funkcji;
• całkowanie funkcji metodą Simpsona;
• rozwiązywanie równań liniowych metodą Gaussa.
Literatura:
1. Knuth D.E.: Sztuka programowania –tom 1,2,3 Wydawnictwa Naukowo- Techniczne, 2001;
2. Praca zbiorowa pod redakcją Jerzego Klamki: Laboratorium metod
numerycznych, Skrypt nr 1305 Politechniki Śląskiej w Gliwicach, Gliwice 1987;
3. Wróblewski P.: Algorytmy, struktury danych i techniki programowania; 4. Stożek E.: Metody numeryczne w zadaniach;
5. Inne książki z metod numerycznych…..
Kompletne algorytmy, bez uzasadnienia matematycznego
Idea algorytmu:
Systematyczne przybliżanie się do miejsca zerowego funkcji za pomocą
stycznych do krzywej.
)
;
( '
) (
1 1
1
i i
z f
z f i
i
z
z
) ( z
istop, jeśli
f
Iteracyjnie powtarzamy
Symbol oznacza stałą, gwarantującą zatrzymanie
algorytmu,
z0 jest wartością początkową, f i f’ trzeba znać jawnie.
#include <iostream.h>
#include <math.h>
const double epsilon=0.0001;
double f(double x) //funkcja f(x)=3x2-2 {return 3*x*x-2;}
double fp(double x) //pochodna f’(x)=(3x2-2)’=6x {return 6*x;}
double zero(double x0,double(*f)(double),double(*fp)(double)) {
if(f(x0)<epsilon)return x0;
else
return zero(x0-f(x0)/fp(x0),f,fp);
}
int main() {
cout << "Zero funkcji 3x*x-2 wynosi "<<zero(1,f,fp)<<endl;
//wynik 0,816497 }
Zostały użyte
wskaźniki do funkcji, ale funkcji można użyć też bezpośrednio.
Idea algorytmu:
Załóżmy, że dysponujemy jawnym wzorem funkcji w postaci uwikłanej: F(x,y)=0.
Za pomocą metody Newtona można obliczyć jej wartość dla pewnego x w sposób iteracyjny:
. jesli
stop,
;
1 ) , ( '
) , ( 1
n n
y x F
y x F n
n
y y
y
y
yWartość początkowa y0 powinna być jak najbliższa wartości poszukiwanej y i spełniać warunek: F(x,y)F’y(x,y)>0.
Zalety:
Czasami iloraz znacznie się upraszcza!
. ) ( 2
, )
, (
, 0
) , (
,
2 1
' 1
1 1
2
n n
n y y
y x
y x y
y
y x F
x y
x F
y
Przykład:
#include <iostream.h>
#include <math.h>
const double epsilon=0.00000001;
double wart(double x,double yn) {
double yn1=2*yn-x*yn*yn;
//fabs(x)=|x|, wartość bezwzględna dla danych double if( fabs(yn-yn1)<epsilon)
return yn1;
else
return wart(x,yn1);
}
int main() {
cout << "Wartość funkcji y=1/x dla x=7 wynosi "<<wart(7,0.1);
//funkcja f(x)=3x2-2 }
Efektywne obliczanie wartości wielomianów – schemat Hornera.
Ma on zastosowanie np. w algorytmach kryptograficznych, bo trzeba tam
wykonywać obliczenia na bardzo dużych liczbach całkowitych. Można takie liczby traktować jako współczynniki wielomianów.
12 9876 0002 6000 0000 0054
).
4 5
( ) 6 ( ) 2 ( ) 6 7
8 9
(
2 20 19 18 17 16 12 11 1
21 x x x x x x x x
x
. 54 ) 6000 ( ) 2 ( ) 9876 ( ) 12
( x5 x4 x3 x2
Podstawa x=10
Podstawa x=10000
Wtedy operacje na dużych liczbach zastępujemy algorytmami działającymi na wielomianach.
. ...
)
( x a x a
1x
1a
1x
1a
0W
n n
n n
W(b) – „metoda siłowa”#include <iostream.h>
const n=5; // stopień wielomianu int oblicz(int b, int w[], int rozm) {
int res=0, pot=1;
for(int j=rozm-1;j>=0;j--) {
res+=pot*w[j]; //sumy cząstkowe pot*=b; //następna potęga b }
return res;
}
int main() {
int w[n]={1,4,-2,0,7}; // współczynniki wielomianu cout << oblicz(2,w,n) << endl;
}
... ... .
)
( b a b a
1b a
2b a
1b a
0W
n
n
n
W(b) – schematHornera
#include <iostream.h>
const n=5; // stopień wielomianu
int oblicz_wielomian2(int a,int w[],int rozm) // schemat Hornera
{
int res=w[0];
for(int j=1;j<rozm;res=res*a+w[j++]);
return res;
}
int main() {
int w[n]={1,4,-2,0,7}; // współczynniki wielomianu cout << oblicz_wielomian2(2,w,n) << endl;
}
Idea algorytmu:
Interpolacja funkcji metodą Lagrange’a służy do przybliżania funkcji (np. za pomocą wielomianu określonego stopnia), gdy:
• nie znamy funkcji tylko dysponujemy fragmentem wykresu, tzn. znamy jej wartości dla skończonego zbioru argumentów
•albo też wyliczanie na podstawie wzoru jest zbyt czasochłonne
Interpolacja funkcji f(x) za pomocą F(x).
Idea algorytmu:
Na rysunku na podstawie 7 par punktów udało się obliczyć wielomian F(x). Wielomian interpolacyjny konstruuje się za pomocą tzw. wyznacznika
Vandermonde’a, który pozwala na wyliczenie współczynników.
Jeśli jednak szukamy tylko wartości funkcji w określonym punkcie z, to prostsza jest metoda Lagrange’a:
. )
)...(
)(
( ) (
0 ( ) ( )
1 0
, 0
n
j z x x x
y
n n
j i i
i j j
x
jz x
z x
z z
F
Powyższy wzór łatwo się tłumaczy na kod C++ za pomocą dwóch zagnieżdżonych pętli „for”.
#include <iostream.h>
#include <math.h>
const int n=3; // stopień wielomianu interpolującego // wartośći funkcji (y[i]=f(x[i]))
double x[n+1]={3.0, 5.0, 6.0, 7.0};
double y[n+1]={1.732, 2.236, 2.449, 2.646};
double interpol(double z, double x[n], double y[n]) // zwraca wartość funkcji w punkcie 'z'
{
double wnz=0,om=1,w;
for(int i=0;i<=n;i++) {
om=om*(z-x[i]);
w=1.0;
for(int j=0;j<=n;j++)
if(i!=j) w=w*(x[i]-x[j]);
wnz=wnz+y[i]/(w*(z-x[i]));
}
return wnz=wnz*om;
}
int main() {
double z=4.5;
cout << "Wartość funkcji sqrt(x) w punkcie " << z;
cout << " wynosi " << interpol(z,x,y) <<endl;
}
Idea algorytmu:
Do numerycznego różniczkowania służy tzw. wzór Stirlinga (nie wyprowadzamy!).
Pozwala on wyznaczyć pochodne f’ i f’’ w punkcie x0 dla pewnej funkcji f(x), której wartości znamy w postaci tabelarycznej:
)),...
( , (
)), ( , ( )), (
, (
)), 2 (
, 2
...(x0 h f x0 h x0 h f x0 h x0 f x0 x0 h f x0 h
Tablica różnic centralnych
Idea algorytmu:
Różnice są obliczane w identyczny sposób w całej tabeli, np.:
).
2 (
) (
)
(x0 23 h f x0 h f x0 h
f
Przyjmując upraszczające założenie, że zawsze będziemy obliczali pochodne dla punktu centralnego x=x0 wzór Stirlinga ma postać:
( ) ( ) ( ) ...
) ( ''
...
) ( '
6 90 4 1
12 2 1
1
2
) (
) ( 30
1 2
) (
) ( 6 1 2
) ( ) 1 (
2
2 5 1
2 5 1
2 3 1
2 3 1
2 1 2
1
x f x
f x
f x
f x f
h
h x f h x f h
x f h x f h
x f h x f h
Punktów kontrolnych może być więcej niż 5. W algorytmie poniżej jest 5 punktów, co prowadzi do tablicy różnic centralnych niskiego rzędu.
Uwaga!
•Im mniejsze h, tym większy błąd!
•Za duże h jest niezgodne z ideą metody.
•Metoda nie jest dobra do punktów na krańcach przedziałów zmienności argumentu funkcji.
#include <iostream.h>
#include <math.h>
const int n=5; // rząd obliczanych różnic centralnych wynosi n-1 double t[n][n+1]=
{ {0.8, 4.80}, // pary (x[i], y[i]) dla y=5x*x+2*x
{0.9, 5.85}, //inicjacja tablicy: wpisane są dwie pierwsze kolumny, {1, 7.00}, // a nie wiersze!
{1.1, 8.25}, {1.2, 9.60}
};
struct POCHODNE{double f1,f2;};
POCHODNE stirling(double t[n][n+1])
// funkcja zwraca wartości f'(z) i f''(z) gdzie z jest elementem
// centralnym: tutaj t[2][0], tablica 't' musi być uprzednio centralnie // zainicjowana, jej poprawność nie jest sprawdzana
{
POCHODNE res;
double h=(t[4][0]-t[0][0])/(double)(n-1); // krok argumentu 'x' for(int j=2;j<=n;j++)
for(int i=0;i<=n-j;i++)
t[i][j]=t[i+1][j-1]-t[i][j-1];
res.f1=((t[1][2]+t[2][2])/2.0-(t[0][4]+t[1][4])/12.0)/h;
res.f2=(t[1][3]-t[0][5]/12.0)/(h*h);
return res;
}
int main()
{ POCHODNE res=stirling(t);
cout << "f'=" << res.f1 << ", f''=" << res.f2 <<endl;
}
Idea algorytmu:
Przedstawiamy skomplikowaną funkcję w postaci przybliżonej , prostszej obliczeniowo.
Na danym etapie i trzy kolejne punkty funkcji podcałkowej są przybliżane parabolą.
Stosujemy to do każdego
obszaru
złożonego z 3 kolejnych punktów.
. )
(
2
0
2 1
0
3
) ( ) ( 4 )
( xx
x f x f x
h
fdx x
•Odstępy h muszą być jednakowe.
f
•Całka globalna jest sumą całek cząstkowych.
#include <iostream.h>
#include <math.h>
const int n=4; // ilość punktów= 2n+1
double f[2*n+1]={41, 29, 19, 11, 5, 1, -1, -1, 1};
double simpson(double f[2*n+1], double a, double b)
// funkcja zwraca całkę funkcji f(x) w przedziale [a,b], // której wartości są podane tabelarycznie w 2n+1 punktach {
double s=0,h=(b-a)/(2.0*n);
for(int i=0;i<2*n;i+=2) // skok cp dwa punkty!
s+=h*(f[i]+4*f[i+1]+f[i+2])/3.0;
return s;
}
int main() {
cout << "Wartość całki =" << simpson(f,-5,3) << endl; // 82.667 }
#include <iostream.h>
#include <math.h>
const int n=4; // ilość punktów= 2n+1 double fun(double x)
{
return x*x-3*x+1;
}
// funkcja x*x-3*x+1 w przedziale [-1,3]
double simpson_f(double(*f)(double), // wskaźnik do f(x) double a, double b, int N)
// funkcja zwraca całkę znanej w postaci wzoru funkcji f(x) // w przedziale [a,b], N - ilość podziałów
{
double s=0,h=(b-a)/(double)N;
for(int i=1;i<=N;i++)
s+=h*(fun(a+(i-1)*h)+4*fun(a-h/2.0+i*h)+fun(a+i*h))/3.0;
return s;
}
int main() {
cout << "Wartość całki =" << simpson_f(fun,-5,3,8) << endl; // 82.667 }
Całkowanie funkcji znanej w postaci analitycznej też jest możliwe
Idea algorytmu:
Aby zastosować algorytm Gaussa, musimy najpierw zapisać układ w postaci uporządkowanej. Przykład:
0 2
6 9 5
z y x
y z x
z x
0 1
1 2
6 1
1 1
9 0
5
z y
x
z y
x
z y x
0 6 9
1 1 2
1 1
1
1 0
5
z y x
Dalej, stosujemy metodę eliminacji Gaussa, sprowadzając macierz do macierzy trójkątnej.
0 1
1 2
6 1
1 1
9 0
5
z y
x
z y
x
z y x
6 , 3 6
, 0 1
0
2 , 4 2
, 1 1
0
9 0
5
z y
x
z y
x
z y x
6 , 0 6
, 0 0
0
2 , 4 2
, 1 1
0
9 0
5
z y
x
z y
x
z y x
Eliminacja y z wierszy 1 i 3 Eliminacja x z
wierszy 2 i 3
Idea algorytmu:
Stosujemy redukcję wsteczną, by wyznaczyć zmienne:
2 5 / ) 9
(
3 2 , 4 2
, 1
1 6
, 0 / 6 , 0
z x
z y
z
Niebezpieczeństwa:
•Eliminacja zmiennych może prowadzić do dzielenia przez zero.
•Zamieniamy wtedy wiersze, chyba, że poniżej wiersza i-tego nie ma tej zmiennej różnej od zera.
•Wtedy układ nie ma rozwiązania.
#include <iostream.h>
#include <math.h>
const int N=3;
double x[N];
double a[N][N+1]={ {5 , 0, 1, 9}, {1 , 1,-1, 6}, {2, -1, 1, 0}};
int gauss(double a[N][N+1], double x[N]) { int max;
double tmp;
for(int i=0; i<N; i++) // eliminacja { max=i;
for(int j=i+1; j<N; j++)
if(fabs(a[j][i])>fabs(a[max][i])) max=j;
for(int k=i; k<N+1; k++) // zamiana wierszy wartościami
{ tmp=a[i][k];
a[i][k]=a[max][k];
a[max][k]=tmp;
}
if(a[i][i]==0) return 0; // Układ sprzeczny!
for(j=i+1; j<N; j++)
for(k=N; k>=i; k--) // mnożenie wiersza j przez współczynnik "zerujący":
a[j][k]=a[j][k]-a[i][k]*a[j][i]/a[i][i];
}
cd.
// redukcja wsteczna
for(int j=N-1; j>=0; j--) { tmp=0;
for(int k=j+1; k<=N; k++) tmp=tmp+a[j][k]*x[k];
x[j]=(a[j][N]-tmp)/a[j][j];
}
return 1; // wszystko w porządku!
}
int main() {
if(!gauss(a,x)) cout << "Układ (1) jest sprzeczny!\n";
else
{ cout << "Rozwiązanie:\n";
for(int i=0;i<N;i++)
cout << "x["<<i<<"]="<<x[i] << endl;
} }
Warto poczytać o szybkiej transformacie Fouriera.
Warto poczytać o algorytmach przeszukiwania wzorca.
Warto poczytać o algorytmach kompresji zbiorów.