diff --git a/HW4/HW4.sln b/HW4/HW4.sln new file mode 100644 index 0000000..7b6430a --- /dev/null +++ b/HW4/HW4.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SkipList", "SkipList\SkipList.csproj", "{CE9E0C67-54C7-4201-A3DC-23973756923D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SkipListTests", "SkipListTests\SkipListTests.csproj", "{25793A21-4C06-4F61-902A-E9F69D44B820}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CE9E0C67-54C7-4201-A3DC-23973756923D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE9E0C67-54C7-4201-A3DC-23973756923D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE9E0C67-54C7-4201-A3DC-23973756923D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE9E0C67-54C7-4201-A3DC-23973756923D}.Release|Any CPU.Build.0 = Release|Any CPU + {25793A21-4C06-4F61-902A-E9F69D44B820}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25793A21-4C06-4F61-902A-E9F69D44B820}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25793A21-4C06-4F61-902A-E9F69D44B820}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25793A21-4C06-4F61-902A-E9F69D44B820}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/HW4/SkipList/SkipList.cs b/HW4/SkipList/SkipList.cs new file mode 100644 index 0000000..edca589 --- /dev/null +++ b/HW4/SkipList/SkipList.cs @@ -0,0 +1,292 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace SkipList; + +using System.Collections; + +/// +/// Skip list implementation. +/// +/// Type of elements in the list. +public class SkipList : IList + where T : IComparable +{ + private const int MaxLevel = 16; + private readonly Random random = new(); + private readonly SkipListNode sentinel = new(default, null, null); + private SkipListNode header; + private SkipListNode baseHeader; + private int currentVersion; + + /// + /// Initializes a new instance of the class. + /// + public SkipList() + { + this.baseHeader = new SkipListNode(default, this.sentinel, this.sentinel); + var curr = this.baseHeader; + for (var level = 0; level < MaxLevel; level++) + { + curr = new SkipListNode(default, this.sentinel, curr); + } + + this.header = curr; + } + + /// + /// Initializes a new instance of the class with elements from a collection. + /// + /// The collection to initialize from. + public SkipList(IEnumerable collection) + : this() + { + ArgumentNullException.ThrowIfNull(collection); + foreach (var element in collection) + { + this.Add(element); + } + } + + /// + public int Count { get; private set; } + + /// + public bool IsReadOnly => false; + + /// + public T this[int index] + { + get + { + if (index < 0 || index >= this.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + var curr = this.baseHeader.Next ?? throw new SkipListException("Cannot access item in empty skip list."); + for (var i = 0; i < index; i++) + { + curr = curr.Next ?? throw new InvalidOperationException("Index exceeds list bounds."); + } + + return curr.Value ?? throw new InvalidOperationException("Null value encountered at index."); + } + set => throw new NotImplementedException(); + } + + /// + public IEnumerator GetEnumerator() + { + var curr = this.baseHeader.Next; + var startVersion = this.currentVersion; + while (curr != this.sentinel && curr != null) + { + if (startVersion != this.currentVersion) + { + throw new InvalidOperationException("Collection modified during enumeration."); + } + + yield return curr.Value ?? throw new NullReferenceException("Element value is null."); + curr = curr.Next; + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + /// + public void Add(T item) + { + ArgumentNullException.ThrowIfNull(item); + var level = this.GenerateRandomLevel(); + var curr = this.header; + var updateNodes = new SkipListNode[level]; + for (var i = 0; i <= MaxLevel - level; i++) + { + curr = curr.Down ?? curr; + } + + for (var lvl = level - 1; lvl >= 0; lvl--) + { + while (curr != null && curr.Next != null && curr.Next != this.sentinel && curr.Next.Value != null && + curr.Next.Value.CompareTo(item) < 0) + { + curr = curr.Next; + } + + updateNodes[lvl] = curr ?? throw new InvalidOperationException("Cannot add null to skip list."); + curr = curr.Down; + } + + SkipListNode? lower = null; + for (var lvl = 0; lvl < level; lvl++) + { + var newNode = new SkipListNode(item, updateNodes[lvl].Next, lower); + updateNodes[lvl].Next = newNode; + lower = newNode; + } + + this.Count++; + this.currentVersion++; + } + + /// + public void Clear() + { + this.baseHeader = new SkipListNode(default, this.sentinel, this.sentinel); + var curr = this.baseHeader; + for (var level = 1; level < MaxLevel; level++) + { + curr = new SkipListNode(default, this.sentinel, curr); + } + + this.header = curr; + this.Count = 0; + this.currentVersion++; + } + + /// + public bool Contains(T item) + { + ArgumentNullException.ThrowIfNull(item); + var curr = this.header; + while (curr != null) + { + while (curr.Next != null && curr.Next != this.sentinel && + curr.Next.Value != null && + curr.Next.Value.CompareTo(item) < 0) + { + curr = curr.Next; + } + + if (curr.Next != null && curr.Next != this.sentinel && + curr.Next.Value != null && + curr.Next.Value.CompareTo(item) == 0) + { + return true; + } + + curr = curr.Down; + } + + return false; + } + + /// + public void CopyTo(T[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + if (arrayIndex < 0 || arrayIndex >= array.Length || arrayIndex + this.Count > array.Length) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + var curr = this.baseHeader.Next; + var idx = arrayIndex; + while (curr != this.sentinel && curr != null) + { + array[idx++] = curr.Value ?? throw new InvalidOperationException("Null element in list."); + curr = curr.Next; + } + } + + /// + public bool Remove(T item) + { + ArgumentNullException.ThrowIfNull(item); + var curr = this.header; + var removed = false; + while (curr != null) + { + while (curr.Next != null && curr.Next != this.sentinel && + curr.Next.Value != null && + curr.Next.Value.CompareTo(item) < 0) + { + curr = curr.Next; + } + + if (curr.Next != null && curr.Next != this.sentinel && + curr.Next.Value != null && + curr.Next.Value.CompareTo(item) == 0) + { + curr.Next = curr.Next.Next; + removed = true; + } + + curr = curr.Down; + } + + if (removed) + { + this.Count--; + this.currentVersion++; + } + + return removed; + } + + /// + public int IndexOf(T item) + { + ArgumentNullException.ThrowIfNull(item); + var idx = 0; + var curr = this.baseHeader.Next; + while (curr != null && curr != this.sentinel) + { + if (curr.Value != null && curr.Value.CompareTo(item) == 0) + { + return idx; + } + + if (curr.Value != null && curr.Value.CompareTo(item) > 0) + { + return -1; + } + + curr = curr.Next; + idx++; + } + + return -1; + } + + /// + public void Insert(int index, T item) + { + throw new NotSupportedException("Insertion at specific index not supported in sorted skip list."); + } + + /// + public void RemoveAt(int index) + { + if (index < 0 || index >= this.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + var item = this[index]; + this.Remove(item); + } + + private int GenerateRandomLevel() + { + var lvl = 1; + while (this.random.NextDouble() < 0.5 && lvl < MaxLevel) + { + lvl++; + } + + return lvl; + } + + private class SkipListNode(T? value, SkipListNode? next, SkipListNode? down) + { + public T? Value { get; set; } = value; + + public SkipListNode? Next { get; set; } = next; + + public SkipListNode? Down { get; set; } = down; + } +} \ No newline at end of file diff --git a/HW4/SkipList/SkipList.csproj b/HW4/SkipList/SkipList.csproj new file mode 100644 index 0000000..7b2dade --- /dev/null +++ b/HW4/SkipList/SkipList.csproj @@ -0,0 +1,22 @@ + + + + Library + net9.0 + enable + enable + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/HW4/SkipList/SkipListException.cs b/HW4/SkipList/SkipListException.cs new file mode 100644 index 0000000..bce9e73 --- /dev/null +++ b/HW4/SkipList/SkipListException.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// +namespace SkipList; + +/// +/// Exception for empty skip list operations. +/// +public class SkipListException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The error message. + public SkipListException(string message) + : base(message) + { + } +} \ No newline at end of file diff --git a/HW4/SkipList/stylecop.json b/HW4/SkipList/stylecop.json new file mode 100644 index 0000000..76c8e76 --- /dev/null +++ b/HW4/SkipList/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "khusainovilas", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +} \ No newline at end of file diff --git a/HW4/SkipListTests/SkipListTests.cs b/HW4/SkipListTests/SkipListTests.cs new file mode 100644 index 0000000..9b66a8b --- /dev/null +++ b/HW4/SkipListTests/SkipListTests.cs @@ -0,0 +1,282 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace SkipList.Tests; + +/// +/// Tests for SkipList. +/// +[TestFixture] +public class SkipListTests +{ + /// + /// Checks that the constructor accepting the collection creates a sorted list. + /// + [Test] + public void SkipList_Constructor_WithUnsortedArray_CreatesSortedList() + { + var list = new SkipList([73, 12, 99, 33, 5]); + + Assert.That(list, Is.EqualTo(new[] { 5, 12, 33, 73, 99 }).AsCollection); + } + + /// + /// Checks that consecutive Add calls preserve the sorting order. + /// + [Test] + public void SkipList_Add_MultipleStrings_KeepsSortedOrder() + { + var list = new SkipList + { + "python", + "java", + "rust", + "csharp", + "go", + }; + + Assert.That(list, Is.EqualTo(new[] { "csharp", "go", "java", "python", "rust" }).AsCollection); + } + + /// + /// Checks the operation of Contains after adding 2000 elements. + /// + [Test] + public void SkipList_Add_TwoThousandIntegers_ContainsReturnsCorrectResult() + { + var list = new SkipList(); + + for (var i = 2000; i >= 1; i--) + { + list.Add(i); + } + + Assert.Multiple(() => + { + Assert.That(list.Contains(42), Is.True); + Assert.That(list.Contains(1337), Is.True); + Assert.That(list.Contains(2000), Is.True); + Assert.That(list.Contains(2001), Is.False); + }); + } + + /// + /// Checks that adding null throws an ArgumentNullException. + /// + [Test] + public void SkipList_Add_NullReference_ThrowsArgumentNullException() + { + var list = new SkipList(); + + Assert.Throws(() => list.Add(null!)); + } + + /// + /// Checks the correctness of the Count property after several inserts. + /// + [Test] + public void SkipList_Count_AfterFourAdds_ReturnsFour() + { + var list = new SkipList + { + 9.81, + 2.718, + 3.14159, + 1.414, + }; + + Assert.That(list.Count, Is.EqualTo(4)); + } + + /// + /// Checks that foreach returns the items in sorted order. + /// + [Test] + public void SkipList_GetEnumerator_UnsortedInput_ReturnsSortedSequence() + { + var list = new SkipList([500, 100, 400, 200, 300]); + + Assert.That(list, Is.EqualTo(new[] { 100, 200, 300, 400, 500 }).AsCollection); + } + + /// + /// Checks that changing the list during enumeration throws an InvalidOperationException. + /// + [Test] + public void SkipList_GetEnumerator_ModifyDuringEnumeration_ThrowsInvalidOperationException() + { + var list = new SkipList([10, 20, 30]); + using var enumerator = list.GetEnumerator(); + + enumerator.MoveNext(); + list.Add(999); + + Assert.Throws(() => enumerator.MoveNext()); + } + + /// + /// Checks that Clear resets Count. + /// + [Test] + public void SkipList_Clear_NonEmptyList_SetsCountToZero() + { + var list = new SkipList([111, 222, 333]); + list.Clear(); + + Assert.That(list.Count, Is.Zero); + } + + /// + /// Checks that the collection is really empty after Clear. + /// + [Test] + public void SkipList_Clear_NonEmptyList_BecomesEmptyCollection() + { + var list = new SkipList(["alpha", "beta", "gamma"]); + list.Clear(); + + Assert.That(list, Is.Empty); + Assert.That(list.Any(), Is.False); + } + + /// + /// Checks the operation of Contains for an existing element. + /// + [Test] + public void SkipList_Contains_ExistingGuid_ReturnsTrue() + { + var guid = Guid.NewGuid(); + var list = new SkipList([Guid.NewGuid(), Guid.NewGuid()]) { guid }; + + Assert.That(list.Contains(guid), Is.True); + } + + /// + /// Checks the correctness of the CopyTo if the array size is sufficient. + /// + [Test] + public void SkipList_CopyTo_SufficientArray_CopiesElementsCorrectly() + { + var list = new SkipList([7, 14, 21, 28]); + var array = new int[7]; + + list.CopyTo(array, 2); + + Assert.That(array, Is.EqualTo(new[] { 0, 0, 7, 14, 21, 28, 0 }).AsCollection); + } + + /// + /// Checks that CopyTo throws an exception if the array size is insufficient. + /// + [Test] + public void SkipList_CopyTo_SmallArray_ThrowsArgumentOutOfRangeException() + { + var list = new SkipList([1, 2, 3]); + var small = new int[2]; + + Assert.Throws(() => list.CopyTo(small, 0)); + } + + /// + /// Checks that the Remove of an existing element returns true and deletes it. + /// + [Test] + public void SkipList_Remove_ExistingString_ReturnsTrueAndRemovesElement() + { + var list = new SkipList(["wolf", "bear", "fox", "lynx"]); + + var removed = list.Remove("bear"); + + Assert.Multiple(() => + { + Assert.That(removed, Is.True); + Assert.That(list.Contains("bear"), Is.False); + Assert.That(list.Count, Is.EqualTo(3)); + }); + } + + /// + /// Checks that the Remove of a non-existent element returns false. + /// + [Test] + public void SkipList_Remove_NonExistingInteger_ReturnsFalse() + { + var list = new SkipList([100, 200, 300]); + + var removed = list.Remove(999); + + Assert.That(removed, Is.False); + Assert.That(list.Count, Is.EqualTo(3)); + } + + /// + /// Checks whether indexOf is valid for an existing element. + /// + [Test] + public void SkipList_IndexOf_ExistingInteger_ReturnsCorrectIndex() + { + var list = new SkipList([150, 250, 350, 450]); + + Assert.That(list.IndexOf(350), Is.EqualTo(2)); + } + + /// + /// Checks that indexOf returns -1 for the missing element. + /// + [Test] + public void SkipList_IndexOf_NonExistingInteger_ReturnsMinusOne() + { + var list = new SkipList([11, 22, 33]); + + Assert.That(list.IndexOf(44), Is.EqualTo(-1)); + } + + /// + /// Checks that Insert always throws a NotSupportedException. + /// + [Test] + public void SkipList_Insert_AnyIndex_ThrowsNotSupportedException() + { + var list = new SkipList(); + + Assert.Throws(() => list.Insert(0, 123)); + } + + /// + /// Checks the deletion of an element by index via RemoveAt. + /// + [Test] + public void SkipList_RemoveAt_MiddleIndex_RemovesCorrectElement() + { + var list = new SkipList([5, 10, 15, 20, 25]); + + list.RemoveAt(2); + + Assert.That(list, Is.EqualTo(new[] { 5, 10, 20, 25 }).AsCollection); + Assert.That(list, Has.No.Member(15)); + } + + /// + /// Checks the receipt of an element by a valid index. + /// + [Test] + public void SkipList_Indexer_GetValidIndex_ReturnsCorrectValue() + { + var list = new SkipList([8, 6, 7, 5, 9]); + + Assert.That(list[2], Is.EqualTo(7)); + } + + /// + /// Checks that accessing an index outside the list throws an ArgumentOutOfRangeException. + /// + [Test] + public void SkipList_Indexer_GetOutOfRangeIndex_ThrowsArgumentOutOfRangeException() + { + var list = new SkipList([1, 2, 3]); + + Assert.Throws(() => _ = list[-1]); + Assert.Throws(() => _ = list[4]); + } +} \ No newline at end of file diff --git a/HW4/SkipListTests/SkipListTests.csproj b/HW4/SkipListTests/SkipListTests.csproj new file mode 100644 index 0000000..019920c --- /dev/null +++ b/HW4/SkipListTests/SkipListTests.csproj @@ -0,0 +1,37 @@ + + + + net9.0 + latest + enable + enable + false + SkipList.Tests + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/HW4/SkipListTests/stylecop.json b/HW4/SkipListTests/stylecop.json new file mode 100644 index 0000000..76c8e76 --- /dev/null +++ b/HW4/SkipListTests/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "khusainovilas", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +} \ No newline at end of file