十大排序算法

概述

1、排序的定义

对一序列对象根据某个关键字进行排序。

2、术语

稳定性:如果 a 等于 b,a 排在 b 的前面,排序后 a 一定仍然在 b 的前面,那么当前排序算法是稳定算法,否则就不是稳定算法。

原地算法:不依赖额外的资源或者依赖少数的额外资源,仅依靠输出来覆盖输入,空间复杂度为 O(1)的都可以认为是原地算法。

3、分类

4、复杂度

名称时间复杂度额外空间复杂度原地算法稳定性
最好最坏平均
冒泡排序O(n)O(n^2)O(n^2)O(1)
选择排序O(n^2)O(n^2)O(n^2)O(1)
插入排序O(n)O(n^2)O(n^2)O(1)
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)
快速排序O(nlogn)O(n^2)O(nlogn)O(logn)
希尔排序O(n)O(n^4/3~n^2)取决于步长序列O(1)
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)
计数排序O(n+k)O(n+k)O(n+k)O(n+k)
基数排序O(d*(n+k))O(d*(n+k))O(d*(n+k))O(n+k)
桶排序O(n+k)O(n+k)O(n+k)O(n+m)

一、冒泡排序

1、执行流程

1、从头开始比较每一个相邻元素,如果第一个比第二个大,就交换它们的位置

执行完一轮后,最末尾那个元素就是最大的元素

2、忽略步骤 1 中曾经找到的最大元素,重复执行步骤 1,直到全部元素有序

2、实现代码

下面是冒泡排序的简单实现: 

protected void sort() {
        // 从后往前遍历,end指针后面的已排序,end前面的未排序
        for (int end = array.length - 1; end > 0; end--) {
            // 比较相邻两个元素
            for (int begin = 0; begin < end; begin++) {
                if (cmp(begin, begin + 1) > 0) {
                    swap(begin, begin + 1);
                }
            }
        }
    }

可以添加一个记录最后一次交换位置的索引,并赋值给 end,end 下次从这个位置开始往前遍历。这样可以提高效率,当数据有序时,遍历一遍外层循环就退出了,最好情况是 O(n)。

    protected void sort() {
         // 从后往前遍历,end指针后面的已排序,end前面的未排序
        for (int end = array.length - 1; end > 0; end--) {
            int sortedIndex = 0;
            // 比较相邻两个元素
            for (int begin = 0; begin < end; begin++) {
                if (cmp(begin, begin + 1) > 0) {
                    swap(begin, begin + 1);
                    sortedIndex = begin + 1;
                }
            }
            end = sortedIndex;
        }
    }

二、快速排序

1、执行流程

1、从序列中选择一个轴点元素(pivot),假设每次选择 0 位置的元素做为轴点元素

2、利用 pivot 将序列分割成 2 个子序列,将小于 pivot 的元素放在 pivot 前面,将大于或等于 pivot 的元素放在 pivot 后面

3、对子序列进行 1,2 循环操作,直到不能再分割(子序列只剩下一个元素)

2、实现代码

public class QuickSort<T extends Comparable<T>> extends Sort<T> {
    @Override
    protected void sort() {
        // [begin,end) 左闭右开区间
        sort(0, array.length);
    }

    private void sort(int begin, int end) {
        if (end - begin < 2) return;
        int mid = pivotIndex(begin, end);
        sort(begin, mid);
        sort(mid + 1, end);
    }

    private int pivotIndex(int begin, int end) {
        T pivot = array[begin];
        // 将左闭右开的end元素指向数组最后一个元素
        end--;
        while (begin < end) {
            while (begin < end) {
                if (cmp(pivot, array[end]) < 0) { // 右边元素 > 轴点元素
                    end--;
                } else { // 右边元素 <= 轴点元素
                    array[begin++] = array[end];
                    break;
                }

            }
            while (begin < end) {
                if (cmp(pivot, array[begin]) > 0) { // 左边元素 < 轴点元素
                    begin++;
                } else { // 左边元素 >= 轴点元素
                    array[end--] = array[begin];
                    break;
                }
            }

        }
        array[begin] = pivot;
        return begin;
    }
}

如果轴点元素一直是最小或最大元素,那么快速排序的时间复杂度为 O(n^2)。

可以将默认选择第一个元素作为轴点元素改成,随机选择一个元素作为轴点元素,然后与第一个元素交换位置,这样只需要一个小改动即可:

// 随机选择一个元素跟begin位置进行交换
swap(begin, begin + (int)(Math.random() * (end - begin)));

T pivot = array[begin];

三、插入排序

1、执行流程

1、在执行过程中,插入排序会将序列分为2部分,头部是已经排好序的,尾部是待排序的

2、从头部开始扫描每一个元素,每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然保持有序

插入排序的时间复杂度与逆序对的数量成正比 

2、实现代码

简单实现: 

private void sort1() {
        for (int begin = 0; begin < array.length; begin++) {
            int cur = begin;
            while (cur > 0 && cmp(cur, cur - 1) < 0) {
                swap(cur, cur - 1);
                cur--;
            }
        }
    }

找到合适位置后再进行替换:

private void sort2() {
        for (int begin = 1; begin < array.length; begin++) {
            int cur = begin;
            // 当前元素
            T v = array[cur];
            while (cur > 0 && cmp(v, array[cur - 1]) < 0) {
                // 前移
                array[cur] = array[cur - 1];
                cur--;
            }
            // 找到合适位置后,将当前元素赋值回去
            array[cur] = v;
        }
    }

四、希尔排序

希尔排序把序列看作是一个矩阵,分成 m 列 ,逐列进行排序

1、实现步骤

1、m 从某个整数逐渐减为1

2、当 m 为1时,整个序列将完全有序

矩阵的列数取决于步长序列,希尔本人给出的步长序列是 n / 2^k

每一列通过插入排序算法排序,从多列编程一列的过程中,逆序对逐渐减少

2、实现代码

protected void sort() {
        List<Integer> stepSequence = shellStepSequence();
        for (Integer step : stepSequence) {
            sort(step);
        }
    }

    /**
     * 分成step列进行排序
     */
    private void sort(int step) {
        // col : 第几列
        for (int col = 0; col < step; col++) { // 对第col列进行排序
            // col、col+step、col+2*step、col+3*step
            for (int begin = col + step; begin < array.length; begin += step) {
                int cur = begin;
                while (cur > col && cmp(cur, cur - step) < 0) {
                    swap(cur, cur - step);
                    cur -= step;
                }
            }
        }
    }

    // 分成 n/2^k 列
    private List<Integer> shellStepSequence() {
        List<Integer> stepSequence = new ArrayList<>();
        int step = array.length;
        while ((step >>= 1) > 0) {
            stepSequence.add(step);
        }
        return stepSequence;
    }

五,选择排序

1、实现步骤

1、从序列中找出最大的那个元素,然后与最末尾的元素交换位置,执行完一轮后,最末尾的那个元素就是最大的元素

2、忽略步骤1中刚刚最大的元素,重复执行步骤1

2、实现代码

protected void sort() {
        for (int end = array.length - 1; end > 0; end--) {
            int max = 0;
            for (int begin = 1; begin <= end; begin++) {
                if (cmp(max, begin) < 0) {
                    max = begin;
                }
            }
            swap(max, end);
        }
    }

六、堆排序

选择排序的思路是找最大元素,然后与末尾元素交换。找最大元素可以使用堆,所以选择排序可以优化成使用堆排序。

1、实现步骤

1、对堆序列进行原地建堆

2、交换栈顶元素与尾元素,堆的元素数量减1,对0位置进行1次下溢操作

3、重复执行步骤2

 

2、实现代码

    protected void sort() {
		// 原地建堆
		heapSize = array.length;
		for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
			siftDown(i);
		}
		while (heapSize > 1) {
			// 交换堆顶元素和尾部元素
			swap(0, --heapSize);
			// 对0位置进行siftDown(恢复堆的性质)
			siftDown(0);
		}
	}

    private void siftDown(int index) {
		T element = array[index];
		
		int half = heapSize >> 1;
		while (index < half) { // index必须是非叶子节点
			// 默认是左边跟父节点比
			int childIndex = (index << 1) + 1;
			T child = array[childIndex];
			
			int rightIndex = childIndex + 1;
			// 右子节点比左子节点大
			if (rightIndex < heapSize && 
					cmp(array[rightIndex], child) > 0) { 
				child = array[childIndex = rightIndex];
			}
			
			// 大于等于子节点
			if (cmp(element, child) >= 0) break;
			
			array[index] = child;
			index = childIndex;
		}
		array[index] = element;
	}

七、归并排序

1、实现步骤

1、不断地将当前序列平均分割成2个序列,直到不能再分割(序列中只有一个元素)

2、不断地将2个序列合并成一个有序序列,最终只剩下1个有序序列

需要 merge 的 2 组序列存在同一个数组中,并且是挨在一起的,为了更好的完成 merge 操作,最好将其中1组序列备份出来,比如左边序列 leftArray ([begin,end))。

 

2、实现代码


	@Override
	protected void sort() {
		leftArray = (T[]) new Comparable[array.length >> 1];
		sort(0, array.length);
	}

	/**
	 * 对 [begin, end) 范围的数据进行归并排序
	 */
	private void sort(int begin, int end) {
		if (end - begin < 2) return;
		
		int mid = (begin + end) >> 1;
		sort(begin, mid);
		sort(mid, end);
		merge(begin, mid, end);
	}
	
	/**
	 * 将 [begin, mid) 和 [mid, end) 范围的序列合并成一个有序序列
	 */
	private void merge(int begin, int mid, int end) {
		int li = 0, le = mid - begin;
		int ri = mid, re = end;
		int ai = begin;
		
		// 备份左边数组
		for (int i = li; i < le; i++) {
			leftArray[i] = array[begin + i];
		}
		
		// 如果左边还没有结束
		while (li < le) { 
			if (ri < re && cmp(array[ri], leftArray[li]) < 0) {
				array[ai++] = array[ri++];
			} else {
				array[ai++] = leftArray[li++];
			}
		}
	}

3、复杂度分析

T(n) = 2 * T( n / 2 )+ O(n) 可以推出归并排序的时间复杂度为 O(nlogn)。下面是常见的递推公式:

递推式复杂度
T(n) =  T( n / 2 )+ O(1)O(logn)
T(n) =T( n -1 )+ O(1)O(n)
T(n) =  T( n / 2 )+ O(n)O(n)
T(n) = 2 * T( n / 2 )+ O(1)O(n)
T(n) = 2 * T( n / 2 )+ O(n)O(nlogn)
T(n) = T( n -1 )+ O(n)O(n^2)
T(n) = 2 * T( n-1 )+ O(1)O(2^n)
T(n) = 2 * T( n -1 )+ O(n)O(2^n)

八、非比较排序

前面的排序都是基于比较的排序,时间复杂度目前最低为 O(nlogn)。

计数排序,桶排序,基数排序都不是基于比较的排序。他们是典型的用空间换时间 ,在某些时候,平均时间复杂度比 O(nlogn) 更低。

1、计数排序

统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引。

实现步骤:

  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i 项;
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  • 反向填充目标数组:将每个元素 i 放在新数组的第 C(i) 项,每放一个元素就将 C(i) 减去1。

上面的计数排序一般实现,有兴趣的可以自行去优化。

 

2、基数排序

基数排序非常适合于整数排序,执行流程如下:

  1. 依次对个位数,十位数,百位数,千位数,万位数...进行排序
  2. 十位数,百位数,千位数,万位数取值范围都是固定的 0~9,可以使用计数排序进行排序

3、桶排序

执行流程

  1. 创建一定数量的痛
  2. 按照一定的规则(不同类型的数据,规则不同),将序列中的元素均匀分配到对应的桶
  3. 分别对每个桶进行单独培训
  4. 将所有非空桶的元素合并成有序序列

例如:

0.340.470.290.840.450.380.350.76

元素在桶中的索引:

可以设定一个规则:元素值 * 元素数量

 

0   
1   
20.340.290.35
30.470.450.38
4   
5   
60.840.76 
7   

然后对每个桶排序,再合并每个桶完成排序

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值