Ders 07 / 11
Algoritmalar
std::sort'un bir vector'ü yerinde nasıl değiştirdiğinin ve std::count gibi salt okuma algoritmaların kaynağı aynen bıraktığının ölçümü.
Yayın Güncelleme
Bu derste
- std::sort'un bir kapsayıcının eleman sırasını yerinde değiştirdiğini ölçer
- Salt okuma algoritmaların (count, find_if, accumulate) kaynağı aynen bıraktığını gösterir
- Karşılaştırma yüklemi geçen algoritmaların özel bir sıralama kuralını nasıl uyguladığını açıklar
- Bir algoritmanın yineleyici çiftiyle çalıştığını ve kapsayıcıya bağımlı olmadığını tanır
İçindekiler
Önceki ders erase sonrası bir yineleyicinin okunmasının belirsiz
olduğunu, erase’in döndürdüğü yeni yineleyicinin ise geçerli kaldığını
gösterdi. O ders tek bir konumu izliyordu. Bu ders sınırı yeniden
genişletiyor: standart kütüphanenin bir kapsayıcının tüm elemanları
üzerinde çalışan hazır algoritmalarına geçiyor. std::sort bir vector’ü
sıralarken kaynağa ne bırakıyor? Aynı soru bu dersteki her algoritma için
tekrar sorulacak; bazı algoritmalar kaynağı okuyup hiç değiştirmezken
bazıları kaynağın kendisini kalıcı olarak yeniden düzenler, ve bu fark
algoritmanın adından değil standart tanımından çıkarılır.
std::sort: Yerinde Değiştiren Bir Algoritma
std::sort, verilen yineleyici aralığındaki elemanları belirtilen
sıralama kuralına göre yerinde düzenler — yeni bir kapsayıcı döndürmez,
verilen aralığın kendisini değiştirir. Varsayılan kural operator<’tir.
// sort-yerinde.cpp — std::sort bir vector'u nasil degistiriyor
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> sayilar{40, 10, 30, 20};
std::sort(sayilar.begin(), sayilar.end());
std::printf("siralanmis dizi:");
for (int deger : sayilar) {
std::printf(" %d", deger);
}
std::printf("\n");
return 0;
}
siralanmis dizi: 10 20 30 40
KT13. std::sort çağrısından sonra sayilar’ın eleman sayısı değişmedi
ama elemanların sırası değişti — bu işlemin sınıfı değişti. Standart,
sıralamadan sonra hangi tam diziliş olacağını garanti eder (küçükten büyüğe,
operator< kuralına göre), yani sonuç belirsiz değil, tam tersine
öngörülebilir ve tekrarlanabilir. Bu, sort’u önceki dersteki erase’den
ayıran şey: erase’in bıraktığı yineleyici belirsizdi, ama sort’un
bıraktığı dizi tanımlıdır.
Salt Okuma Algoritmalar: Kaynağı Aynen Bırakanlar
Her algoritma kaynağı değiştirmez. std::count, verilen değere eşit eleman
sayısını döndürür; std::find_if, bir yüklemi sağlayan ilk elemana bir
yineleyici döndürür; std::accumulate, elemanları biriktirerek toplar.
Üçü de aralığı yalnız okur.
// salt-okuma-algoritmalar.cpp — count, find_if, accumulate kaynagi degistiriyor mu
#include <algorithm>
#include <cstdio>
#include <numeric>
#include <vector>
int main() {
std::vector<int> sayilar{4, 7, 2, 7, 9};
auto once = sayilar;
long adet = std::count(sayilar.begin(), sayilar.end(), 7);
auto it = std::find_if(sayilar.begin(), sayilar.end(),
[](int deger) { return deger > 8; });
int toplam = std::accumulate(sayilar.begin(), sayilar.end(), 0);
std::printf("7 adedi %ld\n", adet);
std::printf("8'den buyuk ilk deger %d\n", *it);
std::printf("toplam %d\n", toplam);
std::printf("kaynak degisti mi %s\n", (sayilar != once) ? "evet" : "hayir");
return 0;
}
7 adedi 2 8'den buyuk ilk deger 9 toplam 29 kaynak degisti mi hayir
KT14. Üç algoritma da sayilar’ı yalnız okudu; işlem sonunda kaynak
işlem öncesindeki kopyasıyla eşit — sınıf aynen. Bu, std::sort’la
doğrudan karşıtlık kurar: ikisi de aynı vector üzerinde çalışır, ama
sort diziliş sırasını değiştirirken bu üç algoritma diziliş sırasına hiç
dokunmaz. Bir algoritmanın adı genelde niyetini gösterir — count, find,
accumulate fiilleri “sorgula” der, sort, reverse, transform fiilleri
“değiştir” der — ama bu bir garanti değil, bir alışkanlıktır; kesin cevap
her zaman ilgili algoritmanın standart tanımındadır.
Bu Kurs Saymaz: Ölçümün Ekseni Çalışma Sayısı Değildir
Trait'ler, Genellikler ve Hata Yönetimi (Rust, M10/K03) bir gövdenin
kaç kez koştuğunu ölçerek soyutlamanın çalışma zamanı bedelini görünür
kılıyordu — orada ölçülen şey bir çağrının kaç kez tekrarlandığıydı. Bu
kurs farklı bir eksende çalışıyor ve saymaz: std::count’un
döndürdüğü 2 gibi bir sayı, aranan değere kaç elemanın eşit olduğunu
söyleyen bir sonuçtur, algoritmanın kaç kez çağrıldığını ya da bir
gövdenin kaç kez çalıştığını sayan bir ölçüm değildir. Bu dersin sorduğu
soru hep aynı: bir işlem kaynağı hangi sınıfa (aynen/değişti/boş/belirsiz)
bıraktı? std::count’un kendisi bir kez çağrılır ve kaynağı aynen
bırakır; içeride kaç karşılaştırma yaptığı bu ölçümün konusu değildir.
Bir okurun iki kursu karıştırmaması için bu ayrım önemlidir: soyutlamanın
çalışma zamanı bedelini öğrenmek isteyen rust-soyutlama’ya, bir işlemin
kaynağı ne durumda bıraktığını öğrenmek isteyen bu kursa bakar.
Özel Karşılaştırma Yüklemi
std::sort’un üçüncü bir argümanı olabilir: iki elemanı karşılaştıran bir
yüklem. Bu yüklem operator<’in yerine geçer ve sıralama kuralını
programcı tanımlar.
// sort-yuklem.cpp — ozel karsilastirma ile buyukten kucuge siralama
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> sayilar{40, 10, 30, 20};
std::sort(sayilar.begin(), sayilar.end(),
[](int a, int b) { return a > b; });
std::printf("buyukten kucuge:");
for (int deger : sayilar) {
std::printf(" %d", deger);
}
std::printf("\n");
return 0;
}
buyukten kucuge: 40 30 20 10
KT15. Yüklem a > b döndürdüğünde a’nın b’den önce geleceğini
belirtir; bu yüzden dizi büyükten küçüğe sıralandı. Bu işlemin sınıfı da
değişti — önceki sort çağrısıyla aynı sınıfta, çünkü ikisi de aynı
algoritmanın farklı bir kuralla çağrılmış hâli. Yüklemin kendisi sayilar’ı
hiç okumadı ya da değiştirmedi; yalnız iki int değeri karşılaştırdı. Bu
ayrım önemli: sort’u değiştirici yapan yineleyici aralığına yazması,
yüklemi değiştirici yapmayan ise yalnız değer karşılaştırması olmasıdır.
Algoritmalar Yineleyici Çiftiyle Çalışır, Kapsayıcıyla Değil
Standart algoritmaların hiçbiri bir kapsayıcı tipi almaz; hepsi bir başlangıç
ve bitiş yineleyicisi alır. Bu, aynı algoritmanın bir vector’ün tamamında,
bir vector’ün bir alt aralığında ya da bir C dizisinde çalışabilmesini
sağlar.
// yineleyici-cifti.cpp — ayni algoritma farkli araliklarda
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> sayilar{5, 3, 8, 1, 9, 2};
std::sort(sayilar.begin(), sayilar.begin() + 3);
std::printf("kismi siralanmis dizi:");
for (int deger : sayilar) {
std::printf(" %d", deger);
}
std::printf("\n");
return 0;
}
kismi siralanmis dizi: 3 5 8 1 9 2
KT16. std::sort(sayilar.begin(), sayilar.begin() + 3) yalnız ilk üç
elemanı sıraladı; kalan üç eleman (1 9 2) girdi sırasında kaldı. Bu, aynı
std::sort çağrısının farklı bir aralıkla farklı bir sonuç ürettiğini
gösteriyor — algoritma kapsayıcının tamamını değil, kendisine verilen
yineleyici çiftini görüyor. Bu işlemin sınıfı yine değişti, çünkü
sayilar’ın bir kısmı (ilk üç eleman) diziliş olarak değişti; ancak
değişikliğin kapsamı bu kez kapsayıcının bütünü değil, verilen aralıkla
sınırlı. Bu tasarımın bir sonucu, algoritmaların kapsayıcı sınırlarını hiç
bilmemesidir: std::sort’a verilen sayilar.begin() + 3 yineleyicisi
kapsayıcının kendi sonunu değil, çağıranın belirttiği sınırı gösteriyor.
Aynı algoritma bir ham C dizisi üzerinde de, işaretçi çiftiyle çağrılarak
aynı şekilde çalışır — kapsayıcı kavramı algoritmanın arayüzünde hiç yer
almaz, yalnız yineleyici kategorisi (burada rastgele erişim) arayüzün
gerektirdiği şeydir.
std::transform: Yeni Bir Hedefe Yazan Algoritma
std::sort kaynak aralığın kendisini değiştiriyordu. std::transform farklı
bir kalıp sunar: kaynağı okur, her elemana bir işlev uygular ve sonucu
ayrı bir hedefe yazar. Kaynak, hedeften ayrı bir kapsayıcı olduğu sürece
std::transform kaynağa hiç dokunmaz.
// transform-ayri-hedef.cpp — std::transform kaynagi mi hedefi mi degistiriyor
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> kaynak{1, 2, 3, 4};
std::vector<int> hedef(kaynak.size());
std::transform(kaynak.begin(), kaynak.end(), hedef.begin(),
[](int deger) { return deger * deger; });
std::printf("kaynak:");
for (int deger : kaynak) {
std::printf(" %d", deger);
}
std::printf("\n");
std::printf("hedef:");
for (int deger : hedef) {
std::printf(" %d", deger);
}
std::printf("\n");
return 0;
}
kaynak: 1 2 3 4 hedef: 1 4 9 16
KT17. kaynak işlem sonunda hâlâ 1, 2, 3, 4 — std::transform
kaynağı yalnız okudu, hiç yazmadı; kaynağın sınıfı aynen. hedef ise
kareler dizisiyle dolduruldu. std::transform’un üçüncü argümanı hedefin
başlangıç yineleyicisidir; bu argüman kaynağın kendisi (kaynak.begin())
olarak da verilebilir — bu durumda algoritma her elemanı okuyup aynı konuma
geri yazar ve kaynak bu kez değişti sınıfına girer. Hangi sınıfın
geçerli olduğu hedefin kaynakla aynı aralık olup olmadığına bağlıdır; bu
ders ikisini ayrı tutarak std::transform’un varsayılan davranışını, yani
kaynağı okuyup ayrı bir yere yazmayı gösteriyor. std::transform’un hedef
argümanı bir yineleyici olduğu için hedefin önceden yeterli boyutta
ayrılmış olması çağıranın sorumluluğundadır — algoritma hedefe eleman
eklemez, yalnız var olan konumlara yazar; hedef kaynak kadar büyük
değilse yazma, ayrılmış bellek sınırının dışına taşar ve bu da tanımsız
davranıştır. Bu ders hedef’i her zaman kaynak.size() ile boyutlandırarak
bu riski hiç oluşturmuyor.
std::stable_sort: Eşit Elemanların Sırasını Koruyan Sıralama
std::sort, eşit kabul edilen iki eleman arasındaki göreli sırayı korumayı
garanti etmez — bu, algoritmanın izin verilen bir yan etkisidir, bir
hata değildir. std::stable_sort aynı sıralama işini yapar ama ek bir
garanti taşır: karşılaştırma yüklemine göre eşit sayılan elemanlar, girdi
sırasındaki göreli konumlarını korur. Bu garanti bedelsiz değildir —
std::sort ortalama O(n log n) karmaşıklıkla çalışırken std::stable_sort
yeterli ek bellek yoksa O(n log² n)’e düşebilir; ek bellek varsa yine
O(n log n) sağlar.
// stable-sort-esitlik.cpp — esit elemanlarin sirasi std::sort ile stable_sort arasinda nasil farkli
#include <algorithm>
#include <cstdio>
#include <utility>
#include <vector>
int main() {
std::vector<std::pair<int, char>> kayitlar{
{2, 'a'}, {1, 'b'}, {2, 'c'}, {1, 'd'}};
std::stable_sort(kayitlar.begin(), kayitlar.end(),
[](const auto& sol, const auto& sag) { return sol.first < sag.first; });
std::printf("siralanmis kayitlar:");
for (const auto& kayit : kayitlar) {
std::printf(" (%d,%c)", kayit.first, kayit.second);
}
std::printf("\n");
return 0;
}
siralanmis kayitlar: (1,b) (1,d) (2,a) (2,c)
KT18. Anahtara göre eşit sayılan {1, 'b'} ve {1, 'd'} çifti,
sıralamadan sonra da girdideki göreli sırasını korudu — b hâlâ d’den
önce geliyor. Aynı şey {2, 'a'} ve {2, 'c'} için de geçerli. Bu
işlemin sınıfı da değişti — dizi baştan sona yeniden düzenlendi — ama
std::sort’tan farkı, eşitler arasındaki göreli sıranın da korunmuş
olduğu ek bir garantidir. std::sort aynı girdide (1,d) ile (1,b)’yi
ters sırada bırakabilirdi; bu, std::sort’un ihlal ettiği bir kural
değil, hiç vermediği bir sözdür.
std::list Neden Kendi sort’unu Taşır
std::sort, rastgele erişim yineleyicisi gerektirir — ortalama O(n log n)
karmaşıklığını sağlayabilmek için elemanlar arasında sabit zamanlı atlama
yapması gerekir. Bu yüzden std::sort, std::list’in çift yönlü
yineleyicisiyle derlenmez. Standart kütüphane bu durum için ayrı bir
üye işlev sağlar: std::list::sort.
// list-sort-uye-islevi.cpp — list icin ayri bir sort neden gerekli
#include <cstdio>
#include <list>
int main() {
std::list<int> degerler{40, 10, 30, 20};
// Asagidaki satir derlenmez, cunku std::sort rastgele erisim yineleyicisi ister:
// std::sort(degerler.begin(), degerler.end());
degerler.sort();
std::printf("siralanmis liste:");
for (int deger : degerler) {
std::printf(" %d", deger);
}
std::printf("\n");
return 0;
}
siralanmis liste: 10 20 30 40
std::list::sort’un ayrı bir üye işlev olarak var olmasının nedeni bu
kısıtlama: list’in düğümleri bitişik bellekte durmadığı için genel
std::sort algoritması onlar üzerinde çalışamaz, ama list kendi
düğümlerinin bağlantılarını yeniden kurarak aynı sonucu farklı bir
karmaşıklıkla üretebilir. Bu, “her kapsayıcı her algoritmayla çalışmaz”
kuralının somut bir örneği. Aynı kısıtlama diğer rastgele-erişim-isteyen
algoritmalar için de geçerlidir: std::nth_element ve ikili aramaya
dayanan std::binary_search, std::lower_bound gibi algoritmalar da
std::list üzerinde doğrudan çağrılamaz; standart kütüphane bu boşluğu
her kapsayıcı için ayrı bir üye işlevle doldurmaz, yalnız list::sort,
list::merge, list::unique ve list::remove gibi düğüm bağlantısı
gerektiren birkaç işlemi üye olarak sunar. Kapsayıcı seçimi bu yüzden
yalnız karmaşıklık garantilerini değil, hangi standart algoritmaların
doğrudan kullanılabileceğini de belirler.
Sınırlayıcı Ölçüm
Aynı veri, aynı anahtara göre karşılaştırma yüklemiyle iki farklı algoritmaya
verildiğinde kaynağın ne kadarı güvenilir biçimde okunabilir sorusu
algoritmanın adına bağlıdır. std::sort, eşit anahtarlı elemanlar
arasındaki göreli sırayı korumayı garanti etmez — bu yüzden o sırayı
yazdırmak, standardın vermediği bir sözü varmış gibi göstermek olur. Aynı
veriyi bir de std::stable_sort ile sıralayıp iki sonucu karşılaştıralım:
#include <algorithm>
#include <cstdio>
#include <utility>
#include <vector>
int main() {
std::vector<std::pair<int, char>> anahtarli{
{2, 'a'}, {1, 'b'}, {2, 'c'}, {1, 'd'}};
std::vector<std::pair<int, char>> kararli = anahtarli;
std::sort(anahtarli.begin(), anahtarli.end(),
[](const auto& sol, const auto& sag) { return sol.first < sag.first; });
std::stable_sort(kararli.begin(), kararli.end(),
[](const auto& sol, const auto& sag) { return sol.first < sag.first; });
std::printf("sort anahtar sirasi:");
for (const auto& kayit : anahtarli) {
std::printf(" %d", kayit.first);
}
std::printf("\n");
std::printf("stable_sort tam sira:");
for (const auto& kayit : kararli) {
std::printf(" %d%c", kayit.first, kayit.second);
}
std::printf("\n");
return 0;
}
sort anahtar sirasi: 1 1 2 2 stable_sort tam sira: 1b 1d 2a 2c
anahtarli üzerinde yalnız anahtarları (1 1 2 2) yazdırdık; harfleri
yazdırmadık, çünkü std::sort’un eşit anahtarlar arasında hangi harfi önce
bıraktığı standart tarafından tanımlanmayan bir ayrıntıdır — aynı derleyici
bile farklı girdi boyutlarında farklı bir iç sıralama uygulayabilir.
kararli üzerinde ise tam kayıtları yazdırabildik, çünkü std::stable_sort
eşit anahtarların girdideki göreli sırasını korumayı garanti eder: 1b
her zaman 1d’den önce, 2a her zaman 2c’den önce gelir. İki çağrı da
aynı veriyi değişti sınıfına taşır — kaynak yerinde yeniden düzenlenir
— ama kaynağın hangi ayrıntısının güvenle okunup bildirilebileceğini
algoritmanın adı belirler: sort yalnız nihai anahtar sırasını, stable_sort
ise anahtar sırasıyla birlikte eşitler arası göreli konumu da garanti eder.
Özet
std::sort, verilen yineleyici aralığındaki elemanların sırasını yerinde değiştirir; bu dersin merkezi ölçümünün sınıfı değişti. Özel bir karşılaştırma yüklemi vermek sıralama kuralını değiştirir ama yüklemin kendisi kaynağı okumaz ya da değiştirmez;std::stable_sortise eşit sayılan elemanların göreli sırasını da korur, bu ek garanti yeterli ek bellek yoksaO(n log² n)’e çıkan bir karmaşıklık bedeliyle gelir. Sınırlayıcı ölçüm: aynı veristd::sortvestd::stable_sort’a verildiğinde ikisi de kaynağı değişti sınıfına taşır, ama kaynağın hangi ayrıntısı güvenle bildirilebilir sorusu algoritmanın adına bağlıdır —sortyalnız anahtar sırasını garanti eder,stable_sorteşitler arası göreli sırayı da.std::count,std::find_if,std::accumulategibi salt okuma algoritmalar kaynağı aynen bırakır.- Bu kurs çalışma sayısı saymaz:
rust-soyutlama’nın “kaç kez koştu” ekseni bu kursun ölçüm ekseni değildir; burada ölçülen şey kaynağın hangi sınıfta kaldığıdır. - Algoritmalar bir kapsayıcı tipi değil, bir yineleyici çifti alır; aynı algoritma farklı aralıklarla farklı kapsamda çalışır.
std::transform, kaynağı okuyup ayrı bir hedefe yazdığında kaynağın sınıfı aynen kalır; hedefi kaynağın kendisi yapmak sınıfı değişti’ye çevirir.std::sort, rastgele erişim yineleyicisi ister vestd::listüzerinde derlenmez;listbunun yerine kendisortüye işlevini sağlar.
Sonraki Adım
Bu ders std::sort’un bir kapsayıcıyı yerinde nasıl değiştirdiğini gösterdi.
Sonraki ders bir kapsayıcıdan eleman kaldırmanın iki adımlı bir deyim
gerektirdiğini ele alıyor: std::remove, elemanları neden gerçekten
silmiyor da yalnız kuyruğa doğru taşıyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.