数据结构-排序算法总结

排序概念总结及算法分类:

排序概念排序是使得一个序列成为按关键字有序的序列的操作
排序稳定性排序过程中排序前顺序和排序中不变的是稳定排序。
内排序和外排序指待排序所有记录是否在内存中操作。外排序是排序记录态度要在内外存之间多次交换。(内排序:插入、交换、选择和归并)
基于比较的排序通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。主要算法有交换(冒泡、快速)、选择(简单选择、堆)、插入(简单插入、希尔排序)、归并(二路、多路归并)。
基于非比较排序不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。主要有计数、桶。

1、冒泡排序

原理理念通过连续地比较与交换相邻元素实现排序,这个过程就像气泡从底部升到顶部一样,因此叫冒泡排序。
算法特性

时间复杂度

n^{2}:n个元素的排序需要遍历的元素个数长度依次为 n−1、n−2、…、2、1 总和为 (n−1)n/2 。不过在引入flag的优化后,最佳时间复杂度可达到 O(n) 。
空间复杂度O(1) 原地排序:只需要常数的额外空间来交换数据。
稳定排序在“冒泡”中遇到相等元素不交换,原有的顺序不会改变。

图解: 

code 实现:

void bubbleSort(int arr[], int size)
{
    for (int i = 0; i < size - 1; ++i)
    {
        bool swaped = false;
        for (int j = 0; j < size - i - 1; ++j)
        {
            if (arr[j] > arr[j + 1])
            {
                swap(arr[j], arr[j + 1]);
                swaped = true;
            }
        }
        if (!swaped)//有数据交换说明已经有序
            break;
    }
}

2、简单选择排序:

原理理念每次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到所有元素均排序完毕。
算法特性

时间复杂度

n^{2}:共 n−1 轮排序,未排序区间长度为 n ,n-1 ... 2 ,但是需要在内层依次比较 n、n−1、…、3、2次,求和为 (n−1)(n+2)2 。。
空间复杂度O(1) 原地排序:只需要常数的额外空间来交换数据。
非稳定排序选出最小的交换,被交换的元素到达的位置可能会改变相同元素的相对顺序

图解: 

code 实现:

void selectSort(int arr[], int size)
{
    for (int i = 0; i < size - 1; ++i)
    {
        int minIndex =i;
        for (int j = i+1; j < size ; ++j)
        {
            if (arr[j] < arr[minIndex ])
            {
                minIndex =j; 
            }
        }
        swap(arr[minIndex], arr[i]);
    }
}


3、插入排序:

原理理念将一个记录插入到已经排好序的有序表中,从而一个新的记录数增 1 的有序表(整理扑克)
算法特性

时间复杂度

n^{2}:在最差情况下逆序,每次插入操作分别需要循环 n−1、n−2、…、2、1 次,需要 (n−1)n/2 。最好是数据完全有序插入操作会提前终止。最佳时间复杂度 O(n) 。
空间复杂度O(1) 原地排序:只需要常数的额外空间来交换数据。
稳定排序在插入操作过程中,会将元素插入到相等元素的右侧,不会改变它们的顺序

图解:

code 实现:

void InsertSort(int arr[], int size)
{
    for (int i = 0; i < size; ++i){
        for (int j = i; j >0&&arr[j]<arr[j-1]; --j)
        {
            swap(arr[j], arr[j - 1]);
        }
    }
}

4、希尔排序

原理理念它是插入排序的改进版,希尔排序又叫缩小增量排序,先将整个待排序列分割成若干子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后逐渐缩小增量,直至为1,最后使用直接插入排序进行最终排序。
算法特性

时间复杂度

希尔排序的时间复杂度为O(n log n),但在最坏情况下的时间复杂度为O(n^2)。希尔排序通常比O(n^2)复杂度的算法快得多‌。
空间复杂度O(1) 原地排序:只需要常数的额外空间来交换数据。
非稳定排序相等的元素在排序后可能会改变它们的相对顺序‌

图解:

code实现:

void shellSort(int arr[], int size) {
    // 从一个大的gap开始,然后在每次迭代中将gap减少一半
    for (int gap = size / 2; gap > 0; gap /= 2) {
        // 对每个gap进行插入排序
        for (int i = gap; i < size; i++) {
            int temp = arr[i];
            for (int j = i; j >= gap && arr[j - gap] > arr[j]; j -= gap) {
                swap(arr[j], arr[j - gap]);
            }
        }
    }
}

5、归并排序:

原理理念

是一种基于分治策略的排序算法,分治:通过递归不断地将待排序从中点处分开,将长原序列分为子序列排序问题。合并:当子序列长度为 1 时终止划分开始合并,持续地将左右两个较短的有序数列合并为一个较长的有序数列,直至结束。

算法特性

时间复杂度

归并排序的总体时间复杂度为O(nlog⁡n),这是所有基于元素比较的排序算法中可达到的最优复杂度。在处理大规模数据时表现出色具有较高的效率。
空间复杂度O(n):元素需暂时在另一个序列中排序,序列的大小等于两个子序列的总长度
稳定排序相等的元素在排序后不会改变它们的相对顺序‌

 图解

 

code 实现:

void mergeSort(int arr[], int left, int right, int temp[])
{
    if (left + 1 >= right)
        return;

    int mid = left + (right - left) / 2;
    mergeSort(arr, left, mid, temp);
    mergeSort(arr, mid, right, temp);
    int p = left, q = mid, i = left;
    while (p < mid || q < right)
    {
        if (q >= right || (p < mid && arr[p] <= arr[q]))
        {
            temp[i++] = arr[p++];
        }
        else
        {
            temp[i++] = arr[q++];
        }
    }
    for (i = left; i < right; ++i)
    {
        arr[i] = temp[i];
    }
}

6、堆排序:

原理理念基于堆数据结构实现的高效排序算法。将待排序序列构建成一个大顶堆,序列最大值就是堆顶元素,将它移走(就是将其与序列末尾元素交换,最大值就确定了)将n-1序列重新构建一个堆,得到次小值,反复“建堆操作”和“元素出堆操作”实现堆排序。
算法特性

时间复杂度

建堆操作使用 O(n) 时间。从堆中提取最大元素的时间复杂度为 O(log⁡n) ,共循环 n−1 轮。它的最坏,最好和平均性能时间复杂度都是O(nlogn)
空间复杂度变量使用 O(1) 空间。元素交换和堆化操作都是在原数组上进行的。
非稳定排序在交换堆顶元素和堆底元素时,相等元素的相对位置可能发生变化。

图解:

堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。(2)大根堆排序算法的基本操作:① 初始化操作:将R[1..n]构造为初始堆。②每一趟排序的基本操作:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。

code 实现:

/* 堆化 */
void siftDown(vector<int> &nums, int n, int i) {
    while (true) {
        // 判断节点 i, left, right 中值最大的节点,记为 max
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        int max = i;
        if (left < n && nums[left] > nums[max])
            max = left;
        if (right < n && nums[right] > nums[max])
            max = right;
        // 若节点 i 最大或索引 left, right 越界则无须继续堆化跳出
        if (max == i) {
            break;
        }
        swap(nums[i], nums[max]); // 循环向下堆化
        i = max;
    }
}

/* 堆排序 */
void heapSort(vector<int> &nums) {
    // 建堆操作:堆化除叶节点以外的其他所有节点
    for (int i = nums.size() / 2 - 1; i >= 0; --i) {
        siftDown(nums, nums.size(), i);
    }
    // 从堆中提取最大元素,循环 n-1 轮
    for (int i = nums.size() - 1; i > 0; --i) {
        swap(nums[0], nums[i]);
        // 以根节点为起点,从顶至底进行堆化
        siftDown(nums, i, 0);
    }
}


7、快速排序:

原理理念

是一种基于分治策略的算法,快速排序的核心操作是“哨兵划分”,其目标是:选择数组中的某个元素作为“基准数”,将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧

算法特性

时间复杂度

在平均情况下O(nlog⁡n) 时间。在最差情况下,每轮哨兵划分操作都将长度为 n 的数组划分为长度为 0 和 n−1 的两个子数组,此时递归层数达到 n ,每层中的循环数为 n ,总体使用 O(n2) 时间。

空间复杂度变量使用 O(n) 空间。在输入数组完全倒序的情况下,达到最差递归深度n,使用 O(n) 栈帧空间。排序操作是在原数组上进行的,未借助额外数组。
非稳定排序在哨兵划分的最后一步,基准数可能会被交换至相等元素的右侧

图解:

code实现:

void quick_sort(vector<int> &nums, int l, int r)
{
    if (l + 1 >= r)
    {
        return;
    }
    int first = l, last = r - 1, key = nums[first];
    while (first < last)
    {
        while (first < last && nums[last] >= key)
        {
            --last;
        }
        nums[first] = nums[last];
        while (first < last && nums[first] <= key)
        {
            ++first;
        }
        nums[last] = nums[first];
    }
    nums[first] = key;
    quick_sort(nums, l, first);
    quick_sort(nums, first + 1, r);
}

 8、计数排序:

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

private static int[] countSort(int[] array,int k)
    {
        int[] C=new int[k+1];//构造C数组
        int length=array.length,sum=0;//获取A数组大小用于构造B数组  
        int[] B=new int[length];//构造B数组
        for(int i=0;i<length;i++)
        {
            C[array[i]]+=1;// 统计A中各元素个数存入C数组
        }
        for(int i=0;i<k+1;i++)//修改C数组
        {
            sum+=C[i];
            C[i]=sum;    
        }
        for(int i=length-1;i>=0;i--)
        {
            B[C[array[i]]-1]=array[i];//将A中该元素放到排序后数组B中指定的位置
            C[array[i]]--;//将C中该元素-1,方便存放下一个同样大小的元素
        }
        return B;//将排序好的数组返回完成排序
    }
    return arr;
}

 计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

9、桶排序:

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

public static void bucketSort(int[] arr){
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for(int i = 0; i < arr.length; i++){
        max = Math.max(max, arr[i]);
        min = Math.min(min, arr[i]);
    }
	
    //桶数
    int bucketNum = (max - min) / arr.length + 1;
    ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
    for(int i = 0; i < bucketNum; i++){
        bucketArr.add(new ArrayList<Integer>());
    }
    //将每个元素放入桶
    for(int i = 0; i < arr.length; i++){
        int num = (arr[i] - min) / (arr.length);
        bucketArr.get(num).add(arr[i]);
    }
    //对每个桶进行排序
    for(int i = 0; i < bucketArr.size(); i++){
        Collections.sort(bucketArr.get(i));
    }
    System.out.println(bucketArr.toString());
}

 桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。 

10、基数排序:

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。1) 取得数组中的最大数,并取得位数;2)arr为原始数组,从最低位开始取每个位组成radix数组;3)对radix进行计数排序(利用计数排序适用于小范围数的特点)

public class RadixSort {
    private static void radixSort(int[] arr) {
        //待排序列最大值
        int max = arr[0];
        int exp;//指数

        //计算最大值
        for (int anArr : arr) {
            if (anArr > max) {
                max = anArr;
            }
        }
        //从个位开始,对数组进行排序
        for (exp = 1; max / exp > 0; exp *= 10) {
            //存储待排元素的临时数组
            int[] temp = new int[arr.length];
            //分桶个数
            int[] buckets = new int[10];
            //将数据出现的次数存储在buckets中
            for (int value : arr) {
                //(value / exp) % 10 :value的最底位(个位)
                buckets[(value / exp) % 10]++;
            }
            //更改buckets[i],
            for (int i = 1; i < 10; i++) {
                buckets[i] += buckets[i - 1];
            }
            //将数据存储到临时数组temp中
            for (int i = arr.length - 1; i >= 0; i--) {
                temp[buckets[(arr[i] / exp) % 10] - 1] = arr[i];
                buckets[(arr[i] / exp) % 10]--;
            }
            //将有序元素temp赋给arr
            System.arraycopy(temp, 0, arr, 0, arr.length);
        }
    }
}

基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。

各排序算法的性能对比

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值