目录
heapify (返回并移除大根堆中最大的值,并保持其依然为大根堆)
题目一 堆
堆结构——用数组实现的完全二叉树结构
大根堆——完全二叉树每棵子树的最大值都在顶部
小根堆——完全二叉树每棵子树的最小值都在顶部
heapInsert (将新数不断插入,并生成大根堆)
插入一个新数时,时间复杂度为O(logN)
当用HeapInsert进行完整堆排序时,时间复杂度为O(N*logN):
若数组为扩容数组(一次扩2倍):
扩容一次的复杂度为O(N),数组要扩容logN次,
所以扩容总代价为O(N*logN),那么每次均摊代价为O(logN)
而heapInsert的复杂度为O(N*logN)
所以总复杂度仍为O(N*logN)+O(logN) = O(N*logN)
namespace DaDingDui
{
class HeapInserts
{
static void Main(string[] args)
{
Console.Write("输入数组长度:");
int len = int.Parse(Console.ReadLine());
int[] intArr = new int[len];
for (int i = 0; i <intArr.Length; i++)
{
Console.Write("插入第{0}个数:", i+1);
string x = Console.ReadLine();
intArr[i] = int.Parse(x);
HeapInsert(intArr, i);
}
Console.Write("大顶堆为:");
foreach (int item in intArr){
Console.Write(item+",");
}
}
public static void HeapInsert(int[] arr, int index)
{
while(arr[index] > arr[(index - 1) / 2])
{
Swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
public static void Swap(int[] arr,int a, int b)
{
//异或写法实例(不一定更快,只是示例新写法)
arr[a] ^= arr[b];
arr[b] ^= arr[a];
arr[a] ^= arr[b];
}
}
}
heapify (返回并移除大根堆中最大的值,并保持其依然为大根堆)
namespace DaDingDui
{
class HeapIfys
{
static void Main(string[] args)
{
Console.Write("输入数组长度:");
int len = int.Parse(Console.ReadLine());
int[] intArr = new int[len];
for (int i = 0; i < intArr.Length; i++)
{
Console.Write("插入第{0}个数:", i + 1);
string x = Console.ReadLine();
intArr[i] = int.Parse(x);
//调用另一个类
HeapInserts.HeapInsert(intArr, i);
}
Console.Write("大顶堆为:");
foreach (int item in intArr)
{
Console.Write(item + ",");
}
//删除最大值
Console.WriteLine();
Console.WriteLine("删除最大值{0}后堆排序为:",intArr[0]);
intArr[0] = intArr[len - 1];
foreach (int item in intArr)
{
HeapIfy(intArr, 0, len - 2);
}
for(int i = 0;i < len - 1;i++ )
{
Console.Write(intArr[i]+ ",");
}
}
/// <summary>
/// 将指定元素自上而下的跟孩子比较大小
/// 并将每次比较的最大值放在父节点处
/// </summary>
/// <param name="arr">数组</param>
/// <param name="index">指定元素下标</param>
/// <param name="heapSize">堆大小</param>
public static void HeapIfy(int[] arr,int index,int heapSize)
{
int left = index * 2 + 1;
while(left < heapSize)
{
//左右孩子的比较
int max = left + 1 < heapSize && arr[left] < arr[left + 1] ?
left + 1 : left;
//父节点和左右孩子较大者的比较
max = arr[max] < arr[index] ? index : max;
if(max == index)
{
break;
}
HeapInserts.Swap(arr,index,max);
index = max;
left = index * 2 + 1;
}
}
}
}
堆排序
时间复杂度:O(N*logN)
using System;
using DaDingDui;
namespace DuiPaixu
{
class HeapSorts
{
/// <summary>
/// 输入数组进行排序测试
/// </summary>
static void Main(string[] args)
{
Console.Write("输入数组长度:");
int len = int.Parse(Console.ReadLine());
int[] intArr = new int[len];
for (int i = 0; i < intArr.Length; i++)
{
Console.Write("插入第{0}个数:", i + 1);
string x = Console.ReadLine();
intArr[i] = int.Parse(x);
HeapSort(intArr);
}
Console.Write("排序后为:");
foreach (int item in intArr)
{
Console.Write(item + ",");
}
}
/// <summary>
/// 堆排序-利用heapify生成大顶堆
/// </summary>
/// <param name="arr">传入数组</param>
public static void HeapSort(int[] arr)
{
if(arr == null || arr.Length < 2)
{
return;
}
for(int i = arr.Length - 1;i >= 0; i--)
{
HeapIfys.HeapIfy(arr, i, arr.Length);
}
}
}
}
堆排序扩展
已知一个几乎有序的数组,几乎有序是指:如果把数组排好顺序的话,每个元素移动的距离可以不超过k,且k相对于数组来说比较小。请对这个数组进行排序
解题:因为元素需要移动的距离不超过k,所以在[0,k]这个范围必存在最小值。将数组的前k-1个元素排好序,之后每加入一个新元素就再排一次序,并弹出最小值。将这个(长度固定的)滑动框不断排序,不断后移。最后将剩余元素弹出。即可得到升序数组。
此题可使用语言自带的PriorityQueue类,当使用系统自带的优先队列时时,相当于使用一个黑盒。它支持插入/ 返回一个数,但它不支持加入(一次多个数),因为它不一定高效,这种情况时,应该自己手写堆排序。
但是—C#对应的.Net 6.0(VS2022)以前的版本都没有这个类,所以需要手写一个PriorityQueue类
public class PriorityQueue<T> where T : IComparable<T> //继承接口
{
//调用比较算法为基本类型的数组进行排序——键值对:键-元素;值-此元素个数
private SortedList<T, int> list = new SortedList<T, int>();
private int count = 0;
//插入元素到此优先队列
public void Add(T item){
//如果已有此键,元素个数+1
if (list.ContainsKey(item)){
list[item]++;
}
//否则添加初始键值对
else{
list.Add(item, 1);
}
//总元素个数+1
count++;
}
//检索并删除队头
public T PopFirst(){
if (Size() == 0)
return default(T);
//获取下标为0的键
T result = list.Keys[0];
//将result元素对应的值-1,如果为0了,将键值对彻底删除
if (--list[result] == 0){
list.RemoveAt(0);
}
count--;
return result;
}
//检索并删除队尾
public T PopLast(){
if (Size() == 0)
return default(T);
int index = list.Count - 1;
T result = list.Keys[index];
Console.WriteLine(list[result]);
if (--list[result] == 0){
list.RemoveAt(index);
}
count--;
return result;
}
//获取队列元总素个数
public int Size(){
return count;
}
//检索并返回队列第一个元素
public T PeekFirst(){
if (Size() == 0)
return default(T);
return list.Keys[0];
}
//检索并返回队列最后一个元素
public T PeekLast(){
if (Size() == 0)
return default(T);
int index = list.Count - 1;
return list.Keys[index];
}
}
}
using System;
namespace DuiPaixu
{
class SortArraysWithKey
{
static void Main(string[] args)
{
//声明一个几乎有序数组(要使数组有序里面的元素最多移动2次)
int[] intArr = { 1, 5, 4, 2, 7, 6 };
SortArrayWithKey(intArr, 2);
//输出排序后的数组
foreach (int item in intArr)
{
Console.Write(item + ",");
}
}
public static void SortArrayWithKey(int[] arr,int k){
//使用C#中小根堆(优先队列)自带函数
PriorityQueue<int> heap = new PriorityQueue<int>();
int index = 0;
//将[0,k)范围内的数添加到顺序队列(小根堆)中
for (;index < Math.Min(arr.Length, k); index++)
{
heap.Add(arr[index]);
}
int i = 0;
//从k开始每添加一个数,弹出一个最小根到数组中
for(;index < arr.Length; i++, index++)
{
heap.Add(arr[index]);
arr[i] = heap.PopFirst();
}
//将最后一个小根堆的其余元素添加到数组中
while (!(heap.Size() == 0))
{
arr[i++] = heap.PopFirst();
}
}
}
}
plus:比较器(重载运算符)
题目二 桶排序
【题目九之前的排序都是基于比较的排序( 两个数比大小 )】
- 桶(容器-队列/ 数组/ 栈)排序:不基于比较的排序
- 应用范围有限:【样本数据状况满足桶的划分时】
- 时间复杂度:O(N)
- (额外)空间复杂度:O(M)
计数排序
(无进制时,且适用于范围较小[100以内的数])
- (1)找原数组A中最大和最小的元素
- (2)统计A中每个值为 k 的元素出现的次数,存入数组B的第 k 项
- (3)将B中的计数累加 ( 某k元素之前的所有元素值之和:B[k] = B[k-1]+B[k] )
- (4)从后向前输出A中元素到新的C数组:将 A 中元素 k 放在新数组C的第 B[k] 项,每放一个元素就将 B[k] 减1
基数排序
(有进制时)
先只看最后一位进行计数排序,然后保持相对次序不变依次往前
/// <summary>
/// 有进制数基数排序(桶排序思想)
/// </summary>
class RadixsSort
{
static void Main(string[] args)
{
int[] arr = { 011, 020, 240, 101, 518, 614, 904 };
RadixSort(arr);
for (int i = 0; i<arr.Length; i++)
{
Console.Write(arr[i]+",");
}
}
public static void RadixSort(int[] arr)
{
if (arr.Length < 2 || arr == null)
{
return;
}
RadixSort(arr, 0, arr.Length - 1, SumBits(arr));
}
/// <summary>
/// 获取数据的最大位数
/// </summary>
public static int SumBits(int[] arr)
{
int sum = int.MinValue;
//找到最大的数
for (int i = 0; i < arr.Length; i++)
{
sum = Math.Max(sum, arr[i]);
}
int res = 0; //记录位数
//统计最大数有几位
while (sum != 0)
{
res++;
sum = sum / 10;
}
return res;
}
//获取数据x第d位的值
public static int getDigit(int x,int d)
{
//将x除到十进制位然后取余
return ( (x/((int)Math.Pow(10,d-1))) %10);
}
/// <summary>
///
/// </summary>
/// <param name="arr">待排序数组</param>
/// <param name="l">第一个元素</param>
/// <param name="r">最后一个元素</param>
/// <param name="digit">最大位数</param>
public static void RadixSort(int[] arr, int l, int r,int digit)
{
int radix = 10; //桶的数量(0~9)
int i = 0, j = 0;
int[] bucket = new int[r - l + 1]; //辅助空间数量
//按第digit位开始排序,再按digit-1位大小排序…… 一直到首位排完
for(int d = 1;d <= digit; d++)
{
//存储第d位是(0-9)的元素的个数——count[i]表示当前d位是i的数字的个数
int[] count = new int[radix];
//统计每个元素第d位的数据
for (i = l; i <= r; i++)
{
j = getDigit(arr[i], d);
count[j]++;
}
//让数组第i位=原数组[0,i-1]数据之和
//这样处理可以最终满足稳定性排序(计数排序的稳定排序类似)
for(i = 1; i < radix; i++)
{
count[i] = count[i] + count[i - 1];
}
//将数组从后向前进行输出排序
for(i = r; i >= l; i--)
{
j = getDigit(arr[i], d);
bucket[count[j] - 1] = arr[i];
count[j]--;
}
//将第d轮桶排序的结果赋值给原数组arr[]
for(i = l,j = 0; i <= r; i++, j++)
{
arr[i] = bucket[j];
}
}
}
}
题目三 排序总结
排序算法的稳定性:值相同的个体,在排完序之后相对次序不变,那么就称这个排序是稳定的
- 不具有稳定性:选择排序、快速排序、堆排序
- 具有稳定性:冒泡、插入、归并、桶排序思想的所有排序
- 在非值类型(struct…)中,稳定性排序的优势:[eg:假如有一堆商品按照质量从优→劣排序,现在想要按照价格从低→高对商品进行排序。使用稳定性排序可以使得价格相同的几件商品保持原有的优→劣顺序,省去了二次排序开销]
时间复杂度 | (额外)空间复杂度 | 稳定性 | |
选择排序 | O(N^2) | O(1) | F |
冒泡排序 | O(N^2) | O(1) | T |
插入排序 | O(N^2) | O(1) | T |
归并排序 | O(N*logN) | O(N) | T |
快速排序 | O(N*logN) | O(logN) | F |
堆排序 | O(N*logN) | O(1) | F |
- 目前没有:时间复杂度O(N* logN),额外空间复杂度O(1),且稳定的排序
- 基于比较的排序,时间复杂度不可能小于O(logN)
- 一般情况下使用快速排序(because常数项低);当有空间的限制时使用堆排序;追求稳定性时使用归并排序
- 快排使用的空间本来是常量级的,但因为递归调用消耗栈空间,所以空间复杂度平均O(logN),最差O(N)
- 系统默认的排序方法:值类型-快排;当为非值类型-手写稳定性排序(归并)扩展
- 改进优化:( 插入+快排结合 )——小样本区域( 范围较小[ eg: <50 ] ),使用插入排序,because小样本时常数时间低;大样本区域,使用快排,because调度优势
【可以,but目前没必要】:
- 快排可以做到稳定性→"01 stable sort "
- 归并排序的额外空间复杂度可以变为O(1)→" 归并排序内部缓存法 "