C# 十大经典排序算法
0、排序总结
0.1 相关概念
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
0.2 算法复杂度
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | $O(n^2)$ | $O(n^2)$ | $O(n)$ | $O(1)$ | 稳定 |
选择排序 | $O(n^2)$ | $O(n^2)$ | $O(n^2)$ | $O(1)$ | 不稳定 |
插入排序 | $O(n^2)$ | $O(n^2)$ | $O(n)$ | $O(1)$ | 稳定 |
希尔排序 | $O(n^{1.3})$ | $O(n^2)$ | $O(n)$ | $O(1)$ | 不稳定 |
归并排序 | $O(nlog_2n)$ | $O(nlog_2n)$ | $O(nlog_2n)$ | $O(n)$ | 稳定 |
快速排序 | $O(nlog_2n)$ | $O(n^2)$ | $O(nlog_2n)$ | $O(nlog_2n)$ | 不稳定 |
堆排序 | $O(nlog_2n)$ | $O(nlog_2n)$ | $O(nlog_2n)$ | $O(1)$ | 不稳定 |
计数排序 | $O(n+k)$ | $O(n+k)$ | $O(n+k)$ | $O(n+k)$ | 稳定 |
桶排序 | $O(n+k)$ | $O(n^2)$ | $O(n)$ | $O(n+k)$ | 稳定 |
基数排序 | $O(n*k)$ | $O(n*k)$ | $O(n*k)$ | $O(n+k)$ | 稳定 |
1、冒泡排序
1.1 工作原理
- 从数组未排序序列的第一个数开始,依次比较相邻元素,如果相邻元素的第一个元素更大则进行交换,直到未排序序列的倒第二个数比较结束,完成一次遍历
- 每完成一次外循环,就会确定未排序序列的最大值,并将这个最大值移动到已排序序列中。所以对于长度为n的数组,至多进行n-1次遍历,就可以实现数组的排序
- 如果在其中一次循环中,没有交换任何相邻元素,说明未排序序列已经是有序的,可以停止循环
1.2 动图演示
1.3 代码实现
// 交换数组中任意两个元素
public static void Swap(int[] arr, int i, int j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static int[] BubbleSort(int[] arr)
{
int count;
//count = BubbleSortNormal(arr);
count = BubbleSortImproved(arr);
return arr;
}
// 常规方法-->(最好,最坏)算法复杂度都是O(n^2)
public static int BubbleSortNormal(int[] arr)
{
int count = 0; // 记录比较的次数==算法时间复杂度
int length = arr.Length; // 数组未排序序列的长度
do
{
for (int j = 0; j < length - 1; j++) // 数组未排序序列的倒第二数
{
count++;
if (arr[j] > arr[j + 1])
{
Swap(arr, j, j + 1);
}
}
length--; // 每次遍历后,就会确定一个最大值,未排序序列的长度减1
}
while (length > 1); // 当数组未排序序列只剩最后一个数时,就不需要排序了
return count;
}
// 优化方法
public static int BubbleSortImproved(int[] arr)
{
int count = 0; // 记录比较的次数==算法时间复杂度
bool swaped; // 判断一次遍历中是否有交换
int length = arr.Length; // 数组未排序序列的长度
do
{
swaped = false; // 每次遍历都初始化没有交换
for (int j = 0; j < length - 1; j++) // 数组未排序序列的倒第二数
{
count++;
if (arr[j] > arr[j + 1])
{
Swap(arr, j, j + 1);
swaped = true;
}
}
length--; // 每次遍历后,就会确定一个最大值,未排序序列的长度减1
}
while (length > 1 && swaped); // 当数组未排序序列只剩最后一个数或者遍历过程中没有交换任何相邻元素时,就不需要排序了
return count;
}
2、选择排序
2.1 工作原理
- 每次遍历,从未排序序列中找到最小值,存放到已排序序列末尾的下一个位置
- 每次遍历,未排序序列长度减1,已排序序列长度加1
- 对于长度为n的数组,遍历n-1次后,未排序序列长度为1,只剩最后一个元素,数组已经有序,无需再遍历
2.2 动图演示
2.3 代码实现
public static int[] SelectionSort(int[] arr)
{
int count = 0;
int minIndex;
int length = arr.Length;
for (int i = 0; i < length - 1; i++) // 遍历n-1次
{
minIndex = i; // 每次遍历,将未排序序列的第一个元素标记为最小值
for (int j = i + 1; j < length; j++)
{
count++;
if (arr[j] < arr[minIndex])
{
minIndex = j;
}
}
if(minIndex != i)
{
Swap(arr, minIndex, i);
}
}
return arr;
}
2.4 总结
稳定性:序列5 8 5 2 9
,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序是不稳定的排序算法。
3、插入排序
3.1 工作原理
- 一开始,认为数组的第一个元素是已排序序列,从数组的第二个元素到最后元素认为是未排序序列
- 每次抓取未排序序列的第一个元素(要插入的元素,并标记空出来的位置为空位置)的值,并标记为要排序的元素值,从后向前遍历已排序序列
- 如果空位置前还有元素,而且空位置的前一个元素大于要排序的元素值,则将空位置的前一个元素后移,空位置前移,直到不满足条件,跳出并结束遍历
- 遍历结束,要排序的元素直接插入到空位置,完成一次插入,则已排序序列长度加1
- 从数组的第二个元素到最后元素一次插入到已排序序列中,插入了n-1次,实现排序
3.2 动图演示
3.3 代码实现
public static int[] InsertionSort(int[] arr)
{
int count;
//InsertionSortNormal(arr);
count = InsertionSortImproved(arr);
return arr;
}
public static int InsertionSortNormal(int[] arr)
{
int count = 0;
int length = arr.Length;
int emptyIndex; // 要排序元素位置
for (int i = 1; i < length; i++)
{
emptyIndex = i;
while(emptyIndex > 0 && arr[emptyIndex] < arr[emptyIndex - 1]) // 要排序元素前有元素,且要排序元素比它前一个元素小
{
count++;
Swap(arr, emptyIndex, emptyIndex - 1); // 交换要排序元素和它前一个元素
emptyIndex--;
}
}
return count;
}
public static int InsertionSortImproved(int[] arr)
{
int count = 0;
int length = arr.Length;
int emptyIndex, // 空位置(空出来的位置)== 要排序元素要插入的位置
currentValue; // 要排序元素的值
for (int i = 1; i < length; i++)
{
emptyIndex = i;
currentValue = arr[i];
while(emptyIndex > 0 && arr[emptyIndex - 1] > currentValue) // 空位置前有元素,且要排序元素比空位置的前一个元素小
{
count++;
arr[emptyIndex] = arr[emptyIndex - 1]; // 空位置的前一个元素后移
emptyIndex--; // 空位置前移
}
arr[emptyIndex] = currentValue; // 将要排序元素的值插入到空位置
}
return count;
}
4、希尔排序
4.1 工作原理
- 希尔排序是改进版的插入排序,它与插入排序的不同之处在于,它会优先比较距离较远的元素
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;对序列进行k 趟排序
- 希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。本文采用动态间隔序列,具体见代码
4.2 动图演示
4.3 代码实现
public static int[] ShellSort(int[] arr)
{
int count = 0;
int length = arr.Length;
int temp, gap = 1;
while(gap < length / 3.0) // 动态定义间隔序列
{
gap = gap * 3 + 1;
}
for (; gap > 0; gap = gap / 3)
{
// 插入排序
for (int i = gap; i < length; i++)
{
temp = arr[i];
int j = i - gap;
for ( ; j >= 0 && arr[j] > temp; j -= gap)
{
count++;
arr[j + gap] = arr[j];
}
arr[j + gap] = temp;
}
}
return arr;
}
5、归并排序
5.1 算法描述
- 归并排序是建立在归并操作上的一种有效的排序算法,是采用分治法(Divide and Conquer)的一个非常典型的应用
- 先把长度为n的输入序列分成两个长度为n/2的子序列,再对这两个子序列分别递归采用归并排序
- 最后将两个排序好的子序列合并成一个最终的排序序列
5.2 动图演示
5.3 代码实现
public static int[] MergeSort(int[] arr)
{
int length = arr.Length;
if (length < 2)
{
return arr;
}
int middle = length / 2;
int[] left = new int[middle], right = new int[length-middle];
Array.Copy(arr, left, middle); // 左序列为[0,...,mindle-1],长度为mindle
Array.Copy(arr, middle, right, 0, length - middle); // 右序列为[mindle,...,length-1],长度为length-middle
return Merge(MergeSort(left), MergeSort(right));
}
// 对两个已排序的子序列进行合并
private static int[] Merge(int[] left, int[] right)
{
int[] result = new int[left.Length + right.Length];
int flagLeft = 0, flagRight = 0, flag = 0;
// 算法同二项式相加的算法类似
while (flagLeft < left.Length && flagRight < right.Length)
{
if (left[flagLeft] <= right[flagRight])
{
result[flag++] = left[flagLeft++];
}
else
{
result[flag++] = right[flagRight++];
}
}
while(flagLeft < left.Length)
{
result[flag++] = left[flagLeft++];
}
while(flagRight < right.Length)
{
result[flag++] = right[flagRight++];
}
return result;
}
6、快速排序
6.1 算法描述
- 快速排序已用到了递归思想
- 每一趟排序,都会确定待排序序列的轴心位置,然后分别递归地将待排序序列中轴心左边和右边的序列进行快速排序,
6.2 动图演示
6.3 代码实现
public static int[] QuickSort(int[] arr)
{
QuickSortCore(arr, 0, arr.Length - 1);
return arr;
}
private static void QuickSortCore(int[] arr, int left, int right)
{
if (left < right) // left==right说明只有一个元素,则已经排序好;left==right+1说明没有元素,不用排序
{
int partitionIndex = Partition(arr, left, right); // 轴心已排序
QuickSortCore(arr, left, partitionIndex - 1); // 递归排序轴心左边序列
QuickSortCore(arr, partitionIndex + 1, right); // 递归排序轴心右边序列
}
}
private static int Partition(int[] arr, int left, int right)
{
int pivot = left; // 轴心点
int index = pivot + 1;
for (int i = index; i <= right; i++)
{
if(arr[i] < arr[pivot])
{
Swap(arr, i, index);
index++;
}
}
Swap(arr, pivot, --index);
// 一轮遍历后,轴心点已排序好
return index;
}
7、堆排序
7.1 算法描述
- 堆排序基于堆结构
- 堆排序分三步:建堆-->依次将堆顶元素放在数组指定位置-->排序完成
7.2 动图演示
7.3 代码实现
public static int[] HeapSort(int[] arr)
{
MaxHeap maxHeap = new MaxHeap(arr); // 建堆
for (int i = arr.Length-1 ; i >= 0 ; i--) // 遍历
{
arr[i] = maxHeap.DeleteMax(); // 依次将堆顶元素排序
}
return arr;
}
/// <summary>
/// 最大堆--用数组表示的完全二叉树,且任一结点的元素值不小于其子结点的元素值
/// </summary>
public class MaxHeap
{
private int[] Data { get; set; } // 存储堆元素的数组
public int Size { get; set; } // 堆中当前元素的个数
public int Capacity { get; set; } // 堆的最大容量
// 构造器
public MaxHeap(int maxSize)
{
if (maxSize < 1)
{
throw new ArgumentException("maxSize must be a positive integer.");
}
Data = new int[maxSize + 1];
Data[0] = int.MaxValue; // 定义哨兵,大于堆中所有可能元素的值
Size = 0;
Capacity = maxSize;
}
public MaxHeap(int[] data)
{
Data = new int[data.Length + 1];
Data[0] = int.MaxValue; // 定义哨兵,大于堆中所有可能元素的值
Size = data.Length;
Capacity = data.Length;
for (int i = 0; i < Size; i++)
{
Data[i + 1] = data[i];
}
int current; // current指向当前要调整的结点,从最后一个有儿子结点的结点开始
current = Size / 2;
for (; current > 0; current--)
{
SiftDown(current);
}
}
// 判断堆是否已满
public bool IsFull()
{
return Size == Capacity;
}
public bool IsEmpty()
{
return Size == 0;
}
/// <summary>
/// 将新增结点插入到从其父节点到根节点的有序序列中
/// </summary>
/// <param name="value"></param>
public void Insert(int value)
{
if (IsFull())
{
Console