算法 | study(二) 堆排序和桶排序

本文详细介绍了堆排序、桶排序以及相关扩展,包括大根堆的插入和删除操作,以及如何处理几乎有序的数组。同时,探讨了各种排序算法的时间复杂度、空间复杂度和稳定性,如快速排序、归并排序和堆排序。此外,还讨论了计数排序和基数排序的实现原理。
摘要由CSDN通过智能技术生成

目录

题目一 堆

heapInsert (将新数不断插入,并生成大根堆)

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)→" 归并排序内部缓存法 "
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值