排序算法(一)

目录

冒泡排序

基础写法 

优化一 

优化二 

复杂度分析 

选择排序 

基础写法 

 复杂度分析 

插入排序

基础写法 

优化一

优化二

二分搜索 

查找待插入的位置

实现 

复杂度分析

归并排序 

merge细节

 代码实现

复杂度分析 


冒泡排序

  1. 从头开始比较每一对相邻元素,如果第1个比第2个大,就交换它们的位置
  2. 执行完一轮后,最末尾那个元素就是最大的元素
  3. 忽略 1 中曾经找到的最大元素,重复执行步骤 1,直到全部元素有序

基础写法 

    public void sort(int[] array) {
		for (int end = array.length - 1; end > 0; end--) {
			for (int begin = 1; begin <= end; begin++) {
				if ((array[begin] < array[begin - 1]) {
                    // 交换下标为begin和begin - 1的元素
					int tmp = array[begin];
		            array[begin] = array[begin - 1];
		            array[begin - 1] = tmp;
				}
			}
		}
	}

优化一 

如果序列已经完全有序,可以提前终止冒泡排序

    public void sort(int[] array) {
		for (int end = array.length - 1; end > 0; end--) {
            // 定义一个升序标志
			boolean sorted = true;
			for (int begin = 1; begin <= end; begin++) {
				if (array[begin] < array[begin - 1]) {
                    // 交换下标为begin和begin - 1的元素
					swap(begin, begin - 1);
					sorted = false;
				}
			}
			if (sorted) break;
		}
	}

优化二 

如果序列尾部已经局部有序,可以记录最后1次交换的位置,减少比较次数 

    public void sort(int[] array) {
		for (int end = array.length - 1; end > 0; end--) {
            // 记录每一轮最后一次交换的位置
			int sortedIndex = 1;
			for (int begin = 1; begin <= end; begin++) {
				if (array[begin] < array[begin - 1]) {
                    // 交换下标为begin和begin - 1的元素
					swap(begin, begin - 1);
					sortedIndex = begin;
				}
			}
            // 更新end
			end = sortedIndex;
		}
	}

复杂度分析 

  • 最坏、平均时间复杂度:O(n2)
  • 最好时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 属于稳定排序

选择排序 

  1. 从序列中最大的那个元素,然后与最末尾的元素交换位置
  2. 执行完一轮后,最末尾的那个元素就是最大的元素
  3. 忽略 1 中曾经找到的最大元素,重复执行步骤 1

基础写法 

	public void sort(int[] array) {
		for (int end = array.length - 1; end > 0; end--) {
            // 记录每一轮最大值
			int max = 0;
			for (int begin = 1; begin <= end; begin++) {
				if (array[max] < array[begin]) {
					max = begin;
				}
			}
            // 交换记录最大值下标和begin的元素
			swap(max, end);
		}
	}

 复杂度分析 

  • 最好、最坏、平均时间复杂度: O(n 2 )
  • 空间复杂度: O(1)
  • 属于不稳定排序

插入排序

  1. 在执行过程中,插入排序会将序列分为2部分
  2. 头部是已经排好序的,尾部是待排序的
  3. 从头扫描每一个元素
  4. 每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然保持有序

基础写法 

	public void sort(int[] array) {
		for(int begin = 1; begin < array.length; begin++) {
            // 依次从下标为 1 的数开始排列顺序
			int cur = begin;
			while(cur > 0 && array[cur] < array[cur - 1]) {
				swap(cur, cur - 1);
                // 当前数的下标--,继续进行比较
				cur--;
			}
		}
	}
注意
  • 插入排序的时间复杂度与逆序对的数量成正比关系,逆序对的数量越多,插入排序的时间复杂度越高
  • 当逆序对的数量极少时,插入排序的效率特别高
  • 数据量不是特别大的时候,插入排序的效率也是非常好的

优化一

思路是将【交换】转为【 挪动 】,将三行语句变为一行
  1. 先将待插入的元素备份
  2. 头部有序数据中比待插入元素大的,都朝尾部方向挪动1个位置
  3. 将待插入元素放到最终的合适位置 
	public void sort(int[] array) {
		for(int begin = 1; begin < array.length; begin++) {
            // 记录合适位置的下标
			int cur = begin;
            // 记录要插入元素的值
			int v = array[cur];
			while(cur > 0 && v - array[cur - 1]) < 0) {
				array[cur] = array[cur - 1];
				cur--;
			}
            // 将待插入元素放到最终的合适位置
			array[cur] = v;
		}
	}

优化二

二分搜索 
如何确定一个元素在数组中的位置?
  • 如果是无序数组,从第 0 个位置开始遍历搜索,平均时间复杂度:O(n)
  • 如果是有序数组,可以使用二分搜索,最坏时间复杂度:O(logn)

思路

假设在 [ begin , end ) 范围内搜索某个元素 v mid == ( begin + end ) / 2
◼ 如果 v < m,去 [ begin , mid ) 范围内二分搜索
如果 v > m,去 [ mid + 1, end ) 范围内二分搜索
如果 v == m,直接返回 mid
◼ 没有找到则返回-1
	public static int indexOf(int[] array, int v) {
		if (array == null || array.length == 0) return -1;
		int begin = 0;
		int end = array.length;
		while (begin < end) {
            // 位运算取中值
			int mid = (begin + end) >> 1;
			if (v < array[mid]) {  // 小于中值end变
				end = mid;
			} else if (v > array[mid]) {  // 大于中值begin变
				begin = mid + 1;
			} else {
				return mid;  //相等返回
			}
		}
		return -1;  // 未找到返回-1
	}
查找待插入的位置
假设在 [ begin , end ) 范围内搜索某个元素 v mid == ( begin + end ) / 2
  •  如果 v < m,去 [begin, mid) 范围内二分搜索
  •  如果 v m,去 [mid + 1, end) 范围内二分搜索
  •  最终返回该位置的下标,没有找到则返回第一个大于v的元素位置

    public static int search(int[] array, int v) {
		if (array == null || array.length == 0) return -1;
		int begin = 0;
		int end = array.length;
		while (begin < end) {
			int mid = (begin + end) >> 1;
			if (v < array[mid]) {
				end = mid;
			} else {
				begin = mid + 1;
			}
		}
		return begin;  // == end
	}
实现 
	public void sort(int[] array) {
		for (int begin = 1; begin < array.length; begin++) {
			insert(begin, search(begin));
		}
	}
	
	// 将source位置的元素插入到dest位置
	private void insert(int source, int dest) {
		T v = array[source];
		for (int i = source; i > dest; i--) {
			array[i] = array[i - 1];
		}
		array[dest] = v;
	}
	
	/**
	 * 利用二分搜索找到 index 位置元素的待插入位置
	 * 已经排好序数组的区间范围是 [0, index)
	 */
	private int search(int index) {
		int begin = 0;
		int end = index;
		while (begin < end) {
			int mid = (begin + end) >> 1;
			if (cmp(array[index], array[mid]) < 0) {
				end = mid;
			} else {
				begin = mid + 1;
			}
		}
		return begin;
	}

   注意: 

        使用了二分搜索后,只是减少了比较次数,但插入排序的平均时间复杂度依然是 O(n2)

复杂度分析

  • 最好、最坏、平均时间复杂度:O(n2)
  • 空间复杂度:O(1)
  • 属于稳定排序

归并排序 

1945年由  约翰·冯·诺伊曼(John von Neumann) 首次提出

执行流程:

  1. 不断地将当前序列平均分割成2个子序列,直到不能再分割(序列中只剩1个元素)
  2. 不断地将2个子序列合并成一个有序序列,直到最终只剩下1个有序序列

merge细节

需要 merge 的 2 组序列存在于同一个数组中,并且是挨在一起的

为了更好地完成 merge 操作,最好将其中 1 组序列备份出来,比如 [begin, mid)

  1. 左边先结束,则数组已经排序完毕,因为两边数组都是有序的
  2. 右边先结束,则只需把左边数组移到右边即可

 代码实现

    public void sort() {
		leftArray = new int[array.length >> 1];
		sort(0, array.length);
	}
	
	// 对 [begin, end) 范围的数据进行归并排序
	private void sort(int begin, int end) {
		// 元素数量 <2
		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 lb = 0, le = mid - begin;    // 左边数组左边界,右边界
		int rb = mid, re = end;    //右边数组左边界,有边界
		int ab = begin; // 往哪个位置覆盖,从begin开始
		
		// 备份左边数组
		for (int i = lb; i < le; i++) {
			// 传进来的参数为begin,不一定是0
			leftArray[i] = array[begin + i];
		}
		
		// 如果左边还没有结束,右边结束则排序完成
		while (lb < le) { 
			// cmp改为 <= 0 会失去稳定性
			// 右边比左边小
			if (rb < re && cmp(array[rb], leftArray[lb]) < 0) {
				array[ab++] = array[rb++]; // 拷贝右边数组到array
			} else { // 右边大于等于左边先拷贝左边,保证稳定性
				array[ab++] = leftArray[lb++]; // 拷贝左边数组到array
			}
		}
	}

复杂度分析 

  • 由于归并排序总是平均分割子序列,所以最好、最坏、平均时间复杂度都是O(nlogn)
  • 属于稳定排序归并排序的空间复杂度是On/2+logn=O(n),n/2用于临时存放左侧数组,logn是因为递归调用
  • 属于稳定排序

  • 34
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值