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;
}
///
/// 最大堆--用数组表示的完全二叉树,且任一结点的元素值不小于其子结点的元素值
///
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;
}
///
/// 将新增结点插入到从其父节点到根节点的有序序列中
///
///
public void Insert(int value)
{
if (IsFull())
{
Console.WriteLine("最大堆已满");
return;
}
// 堆新增空间,并将值插入到新增结点上
Data[++Size] = value;
SiftUp(Size); // 向上调整结点
}
///
/// 从最大堆中取出值为最大的元素,并删除一个结点
///
///
public int DeleteMax()
{
if (IsEmpty())
{
Console.WriteLine("最大堆已空");
return -1;
}
int maxItem = Data[1]; // 取出最大值,即最大堆根节点的值
Data[1] = Data[Size--]; // 将最后一个元素放在堆顶,然后可以从堆顶开始向下调整堆
SiftDown(1);
return maxItem;
}
///
/// 将位置index的元素向上调整
///
/// 要调整的位置
private void SiftUp(int index)
{
int adjustValue = Data[index];
for (; index > 0; index = index / 2)
{
if(Data[index] <= Data[index / 2])
{
break; // 以满足父节点不小于子结点的性质
}
Data[index] = Data[index / 2];
}
Data[index] = adjustValue;
}
///
/// 将位置index的元素向下调整
///
/// 要调整的位置
private void SiftDown(int index)
{
int adjustValue = Data[index];
int current = index, maxChild; // current指向当前结点
for (; current * 2 <= Size; current = maxChild)
{
// 找出最大的儿子结点
maxChild = current * 2;
if ((maxChild != Size) && (Data[maxChild] < Data[maxChild + 1]))
{
maxChild++;
}
// 调整
if (adjustValue >= Data[maxChild])
{
break; // 当前结点不小于最大儿子结点,满足性质跳出循环
}
Data[current] = Data[maxChild];
}
Data[current] = adjustValue;
}
public override string ToString()
{
string result = "";
for (int i = 1; i <= Size; i++)
{
result += Data[i] + " ";
}
return result.Trim();
}
}
8、计数排序
8.1 算法描述
- 计数排序要求输入的数据必须是有确定范围的整数
- 建立确定范围整数的数组C,依次将待排序数组中的值按一定规律存入数组C,数组C中数依次取出成立已排序数组
- 计数排序适合于范围小且值集中的数组
8.2 动图演示
8.3 代码实现
public static int[] CountingSort(int[] arr)
{
CountingSortCore(arr, arr.Min(), arr.Max());
return arr;
}
private static void CountingSortCore(int[] arr, int minValue, int maxValue)
{
int range = maxValue - minValue + 1;
int[] bucket = new int[range]; // 建立长度为待排序数组值范围的数组桶
for (int i = 0; i < bucket.Length; i++)
{
bucket[i] = 0;
}
for (int j = 0; j < arr.Length; j++) // 统计待排序数组每个元素值出现的次数
{
bucket[arr[j] - minValue]++; // 元素值与最小值的差值作为键值,桶数组中对应键的值加1
}
int index = 0;
for (int k = 0; k < bucket.Length; k++) // 反向填充目标数组
{
while (bucket[k] > 0) // 从小到大依次输出桶数组键值和待排序数组最小值之和(即待排序数组原来对应的值),
{
arr[index++] = k + minValue;
bucket[k]--;
}
}
}
9、桶排序
9.1 算法描述
- 桶排序相当于计数排序的升级版,计数排序中的桶对应一个确定的值,而桶排序中的桶对应一定范围的值
- 每个桶进行其他排序或递归使用桶排序
- 最后反向填充到目标数组
9.2 动图演示
9.3 代码实现
public static int[] BucketSort(int[] arr)
{
int bucketSize = 5; // 桶内元素的取值范围
int minValue = arr.Min(), maxValue = arr.Max();
int bucketCount = (maxValue - minValue) / bucketSize + 1;
int[][] buckets = new int[bucketCount][];
int[] helpers = new int[bucketCount]; // 确定每个桶的容量==数组长度
int k = 0;
for (int i = 0; i < arr.Length; i++)
{
k = (arr[i] - minValue) / bucketSize;
helpers[k]++;
}
for (int i = 0; i < buckets.Length; i++)
{
buckets[i] = new int[helpers[i]];
}
for (int i = 0; i < arr.Length; i++) // 目标数组中的值存入对应桶中
{
k = (arr[i] - minValue) / bucketSize;
buckets[k][--helpers[k]] = arr[i];
}
int arrIndex = 0;
for (int j = 0; j < bucketCount; j++)
{
if (buckets[j].Length > 0)
{
InsertionSort(buckets[j]); // 使用插入排序对每个桶进行排序
for (int z = 0; z < buckets[j].Length; z++)
{
arr[arrIndex++] = buckets[j][z]; // 反向填充目标数组
}
}
}
return arr;
}
10、基数排序
1.1 算法描述
- 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。
1.2 动图演示
1.3 代码实现
public static int[] RadixSort(int[] arr)
{
int maxDigit = 0;
int digit, value; // 1代表个位,2代表十位,3代表百位。。。
for (int i = 0; i < arr.Length; i++) // 计算数组中最高位
{
digit = 0;
value = arr[i];
while (value != 0)
{
value /= 10;
digit++;
}
if (digit > maxDigit)
{
maxDigit = digit;
}
}
RadixSortCore(arr, maxDigit);
return arr;
}
private static void RadixSortCore(int[] arr, int maxDigit)
{
int[][] buckets = new int[10][];
int[] helpers = new int[10]; // 每次排序时,对应位上数组的长度
int k = 0;
int mod = 10, dev = 1;
for (int i = 1; i <= maxDigit; i++, mod *= 10, dev *= 10) // 每次排序,都类似于桶排序
{
for (int j = 0; j < buckets.Length; j++)
{
helpers[j] = 0;
}
for (int j = 0; j < arr.Length; j++)
{
k = (arr[j] % mod) / dev;
helpers[k]++;
}
for (int j = 0; j < buckets.Length; j++)
{
buckets[j] = new int[helpers[j]];
helpers[j] = 0;
}
for (int j = 0; j < arr.Length; j++)
{
k = (arr[j] % mod) / dev;
buckets[k][helpers[k]++] = arr[j];
}
int arrIndex = 0;
for (int j = 0; j < buckets.Length; j++)
{
if (buckets[j].Length > 0)
{
for (int z = 0; z < buckets[j].Length; z++)
{
arr[arrIndex++] = buckets[j][z];
}
}
}
}
}