راهنمای تشخیص تکراریها در لیستها
نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۳/۰۹ ۱۰:۱۲
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
bool HasDuplicateBruteForce<T>(IList<T> list)
{
for (int i = 0; i < list.Count - 1; i++)
for (int j = i + 1; j < list.Count; j++)
if (EqualityComparer<T>.Default.Equals(list[i], list[j]))
return true;
return false;
}HashSet یک ساختار داده بهینهسازیشده برای ذخیره مجموعهای از عناصر منحصربهفرد است. این ویژگی آن را به ابزاری عالی برای تشخیص تکراریها تبدیل میکند. با پیمایش لیست و تلاش برای افزودن هر عنصر به یک HashSet، میتوانیم به سرعت تکراریها را شناسایی کنیم، زیرا متد Add در HashSet اگر عنصر از قبل موجود باشد، false را برمیگرداند.bool HasDuplicateHashSet<T>(IEnumerable<T> source)
{
var seen = new HashSet<T>();
foreach (var item in source)
if (!seen.Add(item)) // Add returns false if already present
return true;
return false;
}HashSet<T> به نام seen ایجاد میشود. سپس، حلقه foreach روی source پیمایش میکند. برای هر item، تابع تلاش میکند آن را به seen اضافه کند. اگر Add مقدار false را برگرداند، به این معنی است که item قبلاً در seen وجود داشته است، لذا یک تکراری شناسایی شده و تابع true را برمیگرداند. اگر حلقه کامل شود و هیچ تکراری یافت نشود، false بازگردانده میشود.HashSet<T> به اندازه تعداد عناصر منحصربهفرد.Any() LINQ و HashSet میتوان به روشی مختصر و خوانا تکراریها را تشخیص داد.bool HasDuplicateAny<T>(IEnumerable<T> src)
{
var seen = new HashSet<T>();
return src.Any(item => !seen.Add(item));
}HashSet<T> ایجاد کرده و سپس از متد Any() LINQ استفاده میکند. ()Any روی عناصر src پیمایش میکند و یک شرط را بررسی میکند: !seen.Add(item). اگر Add(item) مقدار false را برگرداند (یعنی عنصر تکراری است)، ()Any بلافاصله true را برمیگرداند و پیمایش متوقف میشود.Any(), All() LINQ نیز میتواند با HashSet ترکیب شود، اما منطق آن کمی متفاوت است. All() بررسی میکند که آیا همه عناصر یک شرط را برآورده میکنند یا خیر. اگر حتی یک عنصر شرط را برآورده نکند، All() مقدار false را برمیگرداند.bool HasDuplicateAll<T>(IEnumerable<T> src)
{
var seen = new HashSet<T>();
return !src.All(item => seen.Add(item)); // All returns false when Add fails
}HashSet<T> ایجاد میکند و از All() LINQ استفاده میکند. شرط در اینجا seen.Add(item) است. اگر Add(item) برای هر عنصری مقدار false را برگرداند (یعنی عنصر تکراری است)، All() بلافاصله false را برمیگرداند. چون ما به دنبال تکراری هستیم، نتیجه All() را نفی میکنیم (!).bool HasDuplicateGroupBy<T>(IEnumerable<T> src) =>
src.GroupBy(x => x).Any(g => g.Count() > 1);Distinct()) برابر نباشد، به این معنی است که لیست شامل تکراریها است.bool HasDuplicateDistinct<T>(ICollection<T> src) =>
src.Count != src.Distinct().Count();HashSet و سپس مقایسه تعداد عناصر HashSet با تعداد عناصر لیست اصلی کار میکند. اگر تعداد متفاوت باشد، تکراری وجود دارد.bool HasDuplicateToHashSet<T>(ICollection<T> src) =>
src.Count != src.ToHashSet().Count;HashSet<T> تبدیل میکند (src.ToHashSet). سپس تعداد عناصر src را با تعداد عناصر HashSet<T> (که فقط شامل عناصر منحصر به فرد است) مقایسه میکند.HashSet<T> کامل به اندازه تمام عناصر منحصر به فرد.ToHashSet() است، با این تفاوت که HashSet مستقیماً با استفاده از سازنده (constructor) آن و با پاس دادن مجموعه اصلی به آن، ساخته میشود.bool HasDuplicateCtor<T>(ICollection<T> src) =>
src.Count != new HashSet<T>(src).Count;HashSet<T> جدید را با استفاده از سازنده آن و با گذراندن تمام عناصر src به آن ایجاد میکند. سپس تعداد عناصر src را با تعداد عناصر HashSet<T> جدید مقایسه میکند.HashSet<T> کامل.HashSet به طور کلی کارآمدترین هستند، بهویژه در لیستهای بزرگ.HashSet (مانند ForEach + HashSet یا Any() + HashSet) بهطور قابل توجهی سریعتر از Brute Force هستند. Brute Force در لیستهای بزرگ به شدت کند میشود.ForEach + HashSet, Any() + HashSet, All() + HashSet) به شدت سریعتر عمل میکنند، زیرا به محض یافتن تکراری، پیمایش را متوقف میکنند.GroupBy(), Distinct(), ToHashSet(), و HashSet Constructor که کل لیست را در حافظه اضافی ذخیره میکنند، مصرف حافظه بالاتری دارند. اگرچه برای لیستهای بزرگتر و تأخیر یکنواخت مفید هستند، اما در سناریوهای حساس به حافظه باید با احتیاط استفاده شوند.ForEach + HashSet بهترین انتخاب است.src.ToHashSet().Count یا new HashSet توصیه میشود. این روشها زمان اجرای ثابتی دارند، بدون توجه به اینکه تکراری کجای لیست قرار دارد.Any() + HashSet انتخاب خوبی است.Brute Force هم قابل قبول است، اما بهتر است عادت استفاده از روشهای کارآمدتر را داشته باشید.Distinct().Count میتواند استفاده شود، اما حتماً عملکرد آن را اندازهگیری کنید.HashSet با خروج زودهنگام استفاده کنید و از GroupBy یا Distinct خودداری کنید.HashSet را هنگام ایجاد آن تنظیم کنید (مانند new HashSet). این کار میتواند تخصیصهای حافظه و بازسازیهای داخلی را کاهش دهد.Brute Force برای اهداف آموزشی مفید است، اما در محیطهای تولیدی بههیچعنوان توصیه نمیشود. روشهای مبتنی بر HashSet، بهویژه آنهایی که از قابلیت خروج زودهنگام پشتیبانی میکنند (مانند ForEach + HashSet و Any() + HashSet)، معمولاً بهترین عملکرد را برای یافتن اولین تکراری ارائه میدهند. در سناریوهایی که تأخیر یکنواخت مهم است، روشهای ToHashSet().Count یا HashSet Constructor میتوانند انتخابهای عالی باشند. در نهایت، همواره توصیه میشود که عملکرد روشهای انتخابی خود را با ابزارهایی مانند Benchmark.NET در محیط واقعی برنامهتان اندازهگیری کنید تا از بهینهترین راهکار برای سناریوی خاص خود اطمینان حاصل کنید. درک دقیق تفاوتهای عملکردی و مصرف حافظه بین این روشها، به توسعهدهندگان داتنت کمک میکند تا کدی کارآمدتر و قابل اطمینانتر بنویسند.| تکنیک | خروج زودهنگام؟ (Early-Exit) | حافظه اضافی؟ (Extra Memory) | پیچیدگی زمانی (Time Complexity) | توضیحات |
| Brute Force (دو حلقه تودرتو) | ✔️ | ❌ | O(n2) | بسیار کند؛ فقط برای مجموعههای بسیار کوچک. |
| ForEach + HashSet | ✔️ | ✔️ | O(n) | کارآمدترین برای یافتن اولین تکراری؛ مصرف حافظه یک HashSet. |
| LINQ Any() + HashSet | ✔️ | ✔️ | O(n) | خوانا، کمی سربار لامبدا، مشابه ForEach + HashSet. |
| LINQ All() + HashSet | ✔️ | ✔️ | O(n) | خوانا، کمی سربار لامبدا، مشابه ForEach + HashSet. |
| LINQ GroupBy() | ❌ | متوسط | O(n) (با سربار) | فاقد خروج زودهنگام، تخصیصهای داخلی اضافی (Lookup). |
| LINQ Distinct().Count | ❌ | متوسط | O(n) (دو پیمایش احتمالی) | ساده، فاقد خروج زودهنگام، مصرف حافظه برای Distinct(). |
| ToHashSet() + Count | ❌ | ✔️ | O(n) | زمان اجرای ثابت، مصرف حافظه کامل HashSet. |
| new HashSet<T>(collection) + Count | ❌ | ✔️ | O(n) | زمان اجرای ثابت، مصرف حافظه کامل HashSet، مشابه ToHashSet(). |