Dizeler .NET'te değiştirilemiyorsa, neden Alt Dizge O (n) zaman alır?


451

Dizeleri .NET değişmez olduğu göz önüne alındığında, neden onlar yerine string.Substring()O ( substring.Length) zaman alır gibi tasarlanmış merak ediyorum O(1)?

yani, eğer ödünleşimler nelerdi?


3
@Mehrdad: Bu soruyu beğendim. .Net'te belirli bir işlevin O () 'sini nasıl belirleyebileceğimizi söyler misiniz? Açık mı yoksa hesaplamalıyız? Thank you
odiseh

1
@odiseh: Bazen (bu durumda olduğu gibi) dizenin kopyalandığı açıktır. Değilse, belgelere bakabilir, karşılaştırmalar yapabilir veya ne olduğunu bulmak için .NET Framework kaynak koduna bakmayı deneyebilirsiniz.
user541686

Yanıtlar:


423

GÜNCELLEME: Bu soruyu çok beğendim, yeni blog yazdım. Bkz. Dizeler, değişmezlik ve kalıcılık


Kısa cevap: n büyüyemezse O (n) O (1) 'dir. Çoğu insan minik dizelerden minik dizeleri çıkarır, böylece karmaşıklığın asemptotik olarak nasıl büyüdüğü tamamen önemsizdir .

Uzun cevap:

Bir örnek üzerindeki işlemlerin, orijinalin belleğinin çok az miktarda (tipik olarak O (1) veya O (lg n)) kopyalama veya yeni ayırma ile yeniden kullanılmasına izin verecek şekilde oluşturulmuş değişmez bir veri yapısı, "kalıcı" olarak adlandırılır değişmez veri yapısı. .NET dizeleri değişmez; sorunuz aslında "neden ısrarcı değiller?"

Çünkü genellikle .NET programlarındaki dizelerde yapılan işlemlere baktığınızda, ilgili her şekilde tamamen yeni bir dize yapmak , hiç de kötü değildir . Karmaşık ve kalıcı bir veri yapısı oluşturmanın maliyeti ve zorluğu kendi başına ödeme yapmaz.

İnsanlar kısa bir dizeyi (örneğin, on ya da yirmi karakteri - biraz daha uzun bir dizeden - belki de birkaç yüz karakterden) ayıklamak için genellikle "alt dize" kullanırlar. Virgülle ayrılmış bir dosyada bir metin satırınız var ve soyadı olan üçüncü alanı ayıklamak istiyorsunuz. Satır belki birkaç yüz karakter uzunluğunda olacak, isim birkaç düzine olacak. Elli baytın dize tahsisi ve bellek kopyalama modern donanımda şaşırtıcı derecede hızlıdır . Varolan bir dizenin ortasına bir işaretçi artı bir uzunluktan oluşan yeni bir veri yapısı oluşturmanın da şaşırtıcı derecede hızlı olması önemsizdir; "Yeterince hızlı" tanımı gereği yeterince hızlı.

Ekstrakte edilen alt ipler tipik olarak küçük boyutlu ve ömür boyu kısa; çöp toplayıcı yakında onları geri alacak ve ilk etapta yığın üzerinde fazla yer kaplamadılar. Dolayısıyla, belleğin çoğunun yeniden kullanılmasını teşvik eden kalıcı bir strateji kullanmak da bir kazanç değildir; Yaptığınız tek şey çöp toplayıcınızı yavaşlatmaktır, çünkü şimdi iç işaretçileri kullanma konusunda endişelenmek zorunda.

İnsanların genellikle tellerde yaptıkları alt dize işlemleri tamamen farklı olsaydı, kalıcı bir yaklaşımla gitmek mantıklı olurdu. İnsanlar tipik olarak milyon karakter dizelerine sahipse ve yüz bin karakter aralığında boyutlarda binlerce örtüşen alt dizeyi çıkarıyorlarsa ve bu alt dizeler öbek üzerinde uzun süre yaşadıysa, kalıcı bir alt dize ile gitmek mükemmel mantıklı olurdu yaklaşmak; savurgan ve aldatıcı olmazdı. Ancak çoğu iş kolu programcısı bu tür şeyler gibi belirsiz bir şey yapmaz. .NET, İnsan Genom Projesinin ihtiyaçlarına göre uyarlanmış bir platform değildir; DNA analiz programcıları her gün bu dizi kullanım özellikleri ile ilgili problemleri çözmek zorundadır; oranlar iyi değil. Yakından maç kendi kalıcı veri yapıları inşa kim kaç onların kullanım senaryolarını.

Örneğin, ekibim siz yazdıkça C # ve VB kodunu anında analiz eden programlar yazar. Bu kod dosyalarından bazıları muazzamdır ve bu nedenle alt dizeleri ayıklamak veya karakter eklemek veya silmek için O (n) dize düzenleme yapamayız. Biz hızlı ve verimli mevcut dize veri kütlesini yeniden kullanmak için bize izin bir metin tampon üzerindeki düzenlemeleri temsil kalıcı iletmenin veri yapılarının bir demet kurmuş ve tipik bir düzenleme üzerinde mevcut sözcük ve sözdizim analizler. Bu, çözülmesi zor bir sorundu ve çözümü, C # ve VB kod düzenlemesinin belirli alan adına dar bir şekilde uyarlandı. Yerleşik dize türünün bu sorunu bizim için çözmesini beklemek gerçekçi olmaz.


47
Java'nın bunu nasıl yaptığını (veya en azından geçmişte bir noktada) kontrastlamak ilginç olurdu: Alt dize yeni bir dize döndürür, ancak daha büyük dize ile aynı karaktere [] işaret eder. alt dize kapsam dışına çıkıncaya kadar artık çöp toplanamaz. .Net'in uygulamasını çok tercih ediyorum.
Michael Stum

13
Bu tür kodları biraz gördüm: string contents = File.ReadAllText(filename); foreach (string line in content.Split("\n")) ...veya diğer sürümleri. Yani bütün bir dosyayı okudum, sonra çeşitli parçaları işledim. Bu tür bir kod çok daha hızlı olurdu ve bir dize kalıcı olsaydı daha az bellek gerektirirdi; her satırı kopyalamak yerine her zaman dosyanın tam olarak bir kopyasına sahip olursunuz, daha sonra her satırın işleminizdeki kısımları kopyalanır. Bununla birlikte, Eric'in dediği gibi - bu tipik kullanım durumu değildir.
konfigüratör

18
@configurator: Ayrıca, .NET 4'te File.ReadLines yöntemi, önce bir belleğin tamamını okumak zorunda kalmadan bir metin dosyasını satırlara ayırır.
Eric Lippert

8
@Michael: Java String, kalıcı bir veri yapısı olarak uygulanır (standartlarda belirtilmez, ancak bildiğim tüm uygulamalar bunu yapar).
Joachim Sauer

33
Kısa cevap: Verilerin bir kopyası orijinal dizenin çöp toplanmasına izin vermek için yapılır .
Qtax

121

Tam olarak Dizeler değişmez olduğundan.Substring , orijinal dizenin en azından bir bölümünün bir kopyasını almalıdır. N baytın bir kopyasını oluşturmak O (n) zamanını almalıdır.

Bir grup baytı sabit zamanda nasıl kopyalayacağınızı düşünüyorsunuz ?


EDIT: Mehrdad dizeyi hiç kopyalamamanızı, ancak bir parçasına referansta bulunmanızı önerir.

Birinin aradığı çok megabaytlık bir dize olan .Net'i düşünün .SubString(n, n+3)(dizenin ortasındaki n için).

Şimdi, ENTIRE dizesi sadece bir referans 4 karaktere bağlı olduğu için Çöp Toplanamaz mı? Bu saçma bir yer kaybı gibi görünüyor.

Ayrıca, alt dizelere yapılan referansların izlenmesi (hatta alt dizelerin içinde bile olabilir) ve GC'yi (yukarıda açıklandığı gibi) yenmekten kaçınmak için en uygun zamanlarda kopyalamaya çalışmak, konsepti bir kabus haline getirir. .SubStringBasit değişmez modeli kopyalamak ve sürdürmek çok daha basit ve daha güvenilirdir .


DÜZENLEME: İşte bu biraz okumak iyi büyük dizeleri içinde altdizgelerin başvurular tutma tehlikesi hakkında.


5
+1: Tam olarak düşüncelerim. Dahili olarak muhtemelen memcpyhala O (n) olanı kullanır .
leppie

7
@abelenky: Sanırım belki de kopyalayarak değil? Zaten orada, neden kopyalamanız gerekiyor?
user541686

2
@Mehrdad: Performans peşindeyseniz. Bu durumda güvensiz ol. Sonra bir char*alt dize alabilirsiniz .
leppie

9
@Mehrdad - orada çok fazla şey bekliyor olabilirsiniz, buna StringBuilder denir ve bir bina dizeleri iyidir . Bu StringMultiPurposeManipulator olarak adlandırılmaz
MattDavey 19:11

3
@SamuelNeff, @Mehrdad: .NET dizeleri vardır değil NULL sonlandırıldı. Lippert'in postunda açıklandığı gibi , ilk 4 bayt ipin uzunluğunu içerir. Skeet'in işaret ettiği gibi, \0karakter içerebilirler .
Elideb

33

Java (.NET'in aksine) iki yol sağlar Substring(), sadece bir referans tutmak mı yoksa tüm alt dizeyi yeni bir bellek konumuna kopyalamak mı istediğinizi düşünebilirsiniz.

Basit .substring(...), dahili olarak kullanılan chardiziyi orijinal String nesnesiyle paylaşır; daha sonra new String(...)gerektiğinde yeni bir diziye kopyalayabilirsiniz (orijinalin çöp toplanmasını engellemek için).

Bu tür bir esnekliğin bir geliştirici için en iyi seçenek olduğunu düşünüyorum.


50
Buna "esneklik" adını veriyorum "Yanlışlıkla teşhis etmek zor bir hata (veya bir performans sorunu) yazılımın içine yerleştirmenin bir yolu çünkü durup bu kodun olabileceği tüm yerleri düşünmem gerektiğini fark etmedim denilen (sadece bir sonraki sürümde icat edilecek olanlar dahil) sadece bir dize ortasından 4 karakter almak için "
Nir

3
aşağı oy geri çekildi ... biraz daha dikkatli kod tarama sonra en azından openjdk sürümü, java bir alt dizesi paylaşılan bir dizi başvuruyor gibi görünüyor. Ve yeni bir dize sağlamak istiyorsanız, bunu yapmanın bir yolu var.
Don Roby

11
@Nir: Ben buna "statüko önyargısı" diyorum. Java yapmanın sizin için riskler ve .Net yolu ile tek mantıklı seçim. Java programcıları için durum tam tersi.
Michael Borgwardt

7
.NET'i kesinlikle tercih ediyorum, ancak bu Java'nın doğru yaptığı bir şey gibi görünüyor. Bir geliştiricinin gerçekten O (1) Substring yöntemine erişmesine izin verilmesi yararlıdır (diğer tüm kitaplıklarla birlikte çalışabilirliği engelleyecek ve yerleşik bir çözüm kadar verimli olmayacak olan kendi dize türünüzü döndürmeden) ). Java'nın çözümü muhtemelen verimsizdir (biri orijinal dize, diğeri alt dize için en az iki yığın nesnesi gerektirir); dilimleri destekleyen diller, ikinci nesneyi yığındaki bir çift işaretçi ile etkili bir şekilde değiştirir.
Qwertie

10
JDK 7u6 beri artık doğru değil - şimdi Java her zaman Dize içeriğini her biri için kopyalar .substring(...).
Xaerxess

12

Java daha büyük dizelere başvururken kullanılır:

Java , bellek sızmasını önlemek için davranışını kopyalama olarak da değiştirdi .

Yine de geliştirilebileceğini hissediyorum: neden kopyalamayı şartlı olarak yapmıyorsunuz?

Alt dize, üst öğenin en az yarısı boyutundaysa, üst öğeye başvurulabilir. Aksi takdirde sadece bir kopyasını alabilirsiniz. Bu, önemli bir fayda sağlarken çok fazla bellek sızmasını önler.


Her zaman kopyalama işlemi dahili diziyi kaldırmanıza olanak tanır. Öbek ayırmalarının sayısını yarıya indirir ve kısa dizelerde ortak bellek tasarrufu sağlar. Ayrıca, her karakter erişimi için ek bir dolaylama atlamanız gerekmediği anlamına gelir.
CodesInChaos

2
Bundan almak için önemli olan şey, Java'nın aslında aynı tabanı char[](başlangıç ​​ve bitiş için farklı işaretçilerle) kullanmaktan yeni bir şey oluşturmaya değişmesidir String. Bu, maliyet-fayda analizinin yeni bir tane yaratmak için bir tercih göstermesi gerektiğini açıkça göstermektedir String.
Filogenez

2

Buradaki yanıtların hiçbiri "basamaklama sorununu" çözmemiştir, yani .NET'teki dizeler bir BStr (bellekte depolanan uzunluk "imlecin önündeki") ve bir CStr (dize bir '\ 0').

"Merhaba orada" dizesi,

0B 00 00 00 48 00 65 00 6C 00 6F 00 20 00 74 00 68 00 65 00 72 00 65 00 00 00

( char*a- fixedstatüsünde bir a atanmışsa, işaretçi 0x48'i gösterir.)

Bu yapı, bir dizenin uzunluğunun hızlı bir şekilde aranmasına izin verir (birçok bağlamda yararlıdır) ve işaretçinin boş sonlandırılmış bir dizgi bekleyen bir P / Invoke to Win32 (veya diğer) API'lerinde geçirilmesine izin verir.

Bunu yaptığınızda Substring(0, 5)size bir kopyasını yapmak gerekir diyor kural "Ah, ama ben son karakteri sonra boş karakterli orada olacağına dair söz". Alt dize sonunda olsa bile, diğer değişkenleri bozmadan uzunluğu koymak için yer olmazdı.


Bununla birlikte, bazen, "dizenin ortası" hakkında gerçekten konuşmak istersiniz ve P / Invoke davranışını önemsemeniz gerekmez. Son eklenen ReadOnlySpan<T>yapı, kopyasız bir alt dize elde etmek için kullanılabilir:

string s = "Hello there";
ReadOnlySpan<char> hello = s.AsSpan(0, 5);
ReadOnlySpan<char> ell = hello.Slice(1, 3);

ReadOnlySpan<char>" Alt dize" uzunluğu bağımsız olarak saklar ve değerin bitiminden sonra bir "\ 0" olduğunu garanti etmez. "Bir dize gibi" birçok şekilde kullanılabilir, ancak BStr veya CStr özelliklerine (her ikisinden de çok daha az) sahip olmadığı için "dize" değildir. Asla (doğrudan) P / Invoke yapmazsanız, çok fazla bir fark yoktur (aramak istediğiniz API'nin ReadOnlySpan<char>aşırı yüklenmesi yoksa ).

ReadOnlySpan<char>bir referans türünün alanı olarak kullanılamaz, bu yüzden de dolaylı bir yol olan ReadOnlyMemory<char>( s.AsMemory(0, 5)) vardır, bu ReadOnlySpan<char>yüzden de aynı farklılıklar stringvardır.

Önceki cevapların bazı cevapları / yorumları, çöp toplayıcının yaklaşık 5 karakter konuşmaya devam ederken bir milyon karakter dizesi tutması gerektiğinin israf edildiğinden bahsetti. Tam olarak bu ReadOnlySpan<char>yaklaşımla elde edebileceğiniz davranış budur . Sadece kısa hesaplamalar yapıyorsanız, ReadOnlySpan yaklaşımı muhtemelen daha iyidir. Bir süre devam etmeniz gerekiyorsa ve orijinal dizenin yalnızca küçük bir yüzdesini koruyacaksanız, (fazla veriyi kesmek için) uygun bir alt dize yapmak muhtemelen daha iyidir. Ortada bir yerde bir geçiş noktası var, ancak bu sizin özel kullanımınıza bağlıdır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.