算法--排序(十三)

以下是学习恋上数据结构与算法的记录,本要内容是排序

◼初识排序
●什么叫排序?
✓排序前:3,1,6,9,2,5,8,4,7
✓排序后:1,2,3,4,5,6,7,8,9(升序)或者9,8,7,6,5,4,3,2,1(降序)

10大排序算法
在这里插入图片描述

◼冒泡排序(Bubble Sort)

●冒泡排序也叫做起泡排序
●执行流程(统一以升序为例子)
①从头开始比较每一对相邻元素,如果第1个比第2个大,就交换它们的位置
✓执行完一轮后,最末尾那个元素就是最大的元素
②忽略①中曾经找到的最大元素,重复执行步骤①,直到全部元素有序

统一的sort

@SuppressWarnings("unchecked")
public abstract class Sort<T extends Comparable<T>> implements Comparable<Sort<T>>{
	protected T[] array;
	public void sort(T[] array) {
		if(array ==null || array.length<2) return;
		this.array=array;
		sort();
	}
	protected abstract void sort();//提供子类调用方法
	//比较培训需要具有可比较性
	public int compareTo(Sort<T> o) {
		int result = (int) (time-o.time);
		if(result!=0) return result;
		result = cmpCount - o.cmpCount;
		if(result!=0) return result;
		return swapCount-o.swapCount;
	}
	protected int cmp(int i1,int i2) {//索引值比较
		cmpCount++;
		return array[i1].compareTo(array[i2]);
	}
	protected int cmp(T v1,T v2) {//值比较
		cmpCount++;
		return v1.compareTo(v2);
	}
	protected void swap(int i1,int i2) {//交换方法
		swapCount++;
		T temp = array[i1];
		array[i1] = array[i2];
		array[i2] = temp;
	}
}

普通冒泡排序
●冒泡排序继承统一的sort,之后的比较排序方法都会继承sort

public class BubbleSort1<T extends Comparable<T>> extends Sort<T>{
	@Override
	protected void sort() {
		for(int end = array.length-1;end>0;end--) {
			for(int begin=1;begin<=end;begin++) {
				if(cmp(begin, begin-1)<0) {
					swap(begin, begin-1);
				}
			}
		}
	}
}

冒泡排序–优化①
●如果序列已经完全有序,可以提前终止冒泡排序

protected void sort() {
		for(int end = array.length-1;end>0;end--) {
			boolean sorted = true;//设置一标签,为true则有序,每轮循环都默认为有序
			for(int begin=1;begin<=end;begin++) {
				if(cmp(begin, begin-1)<0) {
					swap(begin, begin-1);
					sorted = false;//若发现乱序,则为false
				}
			}
			if(sorted) break;
		}
	}

冒泡排序–优化②
●如果序列尾部已经局部有序,可以记录最后1次交换的位置,减少比较次数
在这里插入图片描述

protected void sort() {
		for(int end = array.length-1;end>0;end--) {
			int sortedIndex = 0;
			for(int begin=1;begin<=end;begin++) {
				if(cmp(begin, begin-1)<0) {
					swap(begin, begin-1);
					sortedIndex = begin;//记录最后一次交换的位置
				}
			}
			end = sortedIndex;
		}
	}

◼排序算法的稳定性(Stability)
●如果相等的2个元素,在排序前后的相对位置保持不变,那么这是稳定的排序算法
✓排序前:5,1,3𝑎,4,7,3𝑏
✓稳定的排序:1,3𝑎,3𝑏,4,5,7
✓不稳定的排序:1,3𝑏,3𝑎,4,5,7

对自定义对象进行排序时,稳定性会影响最终的排序效果
●冒泡排序属于稳定的排序算法
但如果是改为 <= ,则两个相等元素的时候,也会发生交换位置行为,最终导致算法为不稳定算法。
稍有不慎,稳定的排序算法也能被写成不稳定的排序算法,比如下面的冒泡排序代码是不稳定的
◼原地算法(In-place Algorithm
●何为原地算法?
✓不依赖额外的资源或者依赖少数的额外资源,仅依靠输出来覆盖输入(如冒泡排序属于In-place)
✓空间复杂度为𝑂(1)的都可以认为是原地算法
●非原地算法,称为Not-in-place 或者Out-of-place

◼选择排序(Selection Sort)

●执行流程①从序列中找出最大的那个元素,然后与最末尾的元素交换位置
✓执行完一轮后,最末尾的那个元素就是最大的元素
②忽略①中曾经找到的最大元素,重复执行步骤①

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);//与最末尾的元素交换位置
		}
	}

●选择排序的交换次数要远远少于冒泡排序,平均性能优于冒泡排序,但属于不稳定排序
思考:选择排序是否还有优化的空间?✓使用堆来选择最大值

堆排序(Heap Sort)

堆排序可以认为是对选择排序的一种优化,不稳定排序
●执行流程
①对序列进行原地建堆(heapify)
②重复执行以下操作,直到堆的元素数量为1
✓交换堆顶元素与尾元素
✓堆的元素数量减1
✓对0 位置进行1 次siftDown 操作
在这里插入图片描述
在这里插入图片描述

public class HeapSort <T extends Comparable<T>> extends Sort<T>{
	private int heapSize;
	@Override
	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;
	}
}

◼插入排序(Insertion Sort)

在这里插入图片描述
●执行流程
①在执行过程中,插入排序会将序列分为2部分
✓头部是已经排好序的,尾部是待排序的
②从头开始扫描每一个元素
✓每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然保持有序

	protected void sort() {
		for(int begin = 1;begin<array.length;begin++) {
			int cur = begin;//用一新的cur记录,也是为了防止后面的加减操作修改了begin值
			while(cur>0 && cmp(cur, cur-1)<0) {
				swap(cur, cur-1);
				cur--;//往头部前面方向比较,直到插入合适位置
			}
		}
		
	}

插入排序–逆序对(Inversion)
●什么是逆序对?
数组<2,3,8,6,1> 的逆序对为:<2,1> ❤️,1> <8,1> <8,6> <6,1>,共5个逆序对
●插入排序的时间复杂度与逆序对的数量成正比关系,逆序对的数量越多,插入排序的时间复杂度越高,当逆序对的数量极少时,插入排序的效率特别高,甚至速度比Onlogn级别的快速排序还要快
●数据量不是特别大的时候,插入排序的效率也是非常好的

插入排序–优化
●思路是将【交换】转为【挪动】
①先将待插入的元素备份
②头部有序数据中比待插入元素大的,都朝尾部方向挪动1个位置
③将待插入元素放到最终的合适位置

	protected void sort() {
		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;//最后覆盖
		}
	}
◼二分搜索(Binary Search)

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

二分搜索–思路
在这里插入图片描述
●假设在[begin, end) 范围内搜索某个元素v,mid == (begin+ end) / 2
✓如果v< m,去[begin, mid) 范围内二分搜索
✓如果v> m,去[mid+ 1, end) 范围内二分搜索
✓如果 v == m,直接返回mid

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)>>2;
		    if(v<array[mid]) {
		    	end = mid;
		    }else if(v>array[mid]) {
		    	begin=mid+1;
		    }else {
		    	return mid;
		    }
		}
		return -1;
	}

插入排序–二分搜索优化
●在元素v 的插入过程中,可以先二分搜索出合适的插入位置,然后再将元素v 插入
✓要求二分搜索返回的插入位置
在这里插入图片描述
插入排序–二分搜索优化–实现

    protected void sort() {
		for (int begin = 1; begin < array.length; begin++) {
			insert(begin, search(begin));
		}
	}
	/**
	 * 将source位置的元素插入到dest位置
	 * @param source
	 * @param 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)
	 * @param index
	 * @return
	 */
	private int search(int index) {
		int begin = 0;
		int end = index;
		while (begin < end) {
			int mid = (begin + end) >> 1;//mid = (begin+end)/2
			if (cmp(array[index], array[mid]) < 0) {
				end = mid;
			} else {
				begin = mid + 1;
			}
		}
		return begin;//begin==end
	}

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

◼归并排序(Merge Sort)

●执行流程
在这里插入图片描述
①不断地将当前序列平均分割成2个子序列
✓直到不能再分割(序列中只剩1个元素)
②不断地将2个子序列合并成一个有序序列
✓直到最终只剩下1个有序序列

归并排序–divide实现

private T[] leftArray;
	@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);
	}

归并排序–merge
在这里插入图片描述
归并排序–merge细节
在这里插入图片描述
●归并排序–merge –左边先结束
如果左边先结束,右边还有数值,则可退出循环,因为右边原本就在array数组上
在这里插入图片描述
●归并排序–merge –右边先结束
如果右边先结束,左边还有数值,则需要继续循环合并,把左边数值赋值到array数组中,所以不需要考虑右边先结束,因为其步骤一样。
在这里插入图片描述
归并排序–merge实现

/**
	 * 将 [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++];
			}
		}
	}
◼快速排序(Quick Sort)

●执行流程
①从序列中选择一个轴点元素(pivot)
✓假设每次选择0 位置的元素为轴点元素

②利用pivot 将序列分割成2 个子序列
✓将小于pivot 的元素放在pivot前面(左侧)
✓将大于pivot 的元素放在pivot后面(右侧)
✓等于pivot的元素放哪边都可以

③对子序列进行①②操作
✓直到不能再分割(子序列中只剩下1个元素)
在这里插入图片描述
◼快速排序的本质是逐渐将每一个元素都转换成轴点元素

●快速排序–轴点构造
在这里插入图片描述
Java代码

@Override
	protected void sort() {
		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);
	}
	/**
	 * 构造出 [begin, end) 范围的轴点元素
	 * @return 轴点元素的最终位置
	 */
	private int pivotIndex(int begin, int end) {
		//随机选择一个元素跟begin位置进行交换,随机轴点
		swap(begin, begin+(int)(Math.random()*(end-begin)));
		//备份begin位置元素
		T privot= array[begin];
		//end指向最后一个元素
		end--;
		//双while与break是为了交替方向
		while (begin<end) {
			while(begin<end) {
				if(cmp(privot, array[end])<0) { // 右边元素 > 轴点元素
					end--;
				}else {// 右边元素 <= 轴点元素
					array[begin++] = array[end];
					break;
				}
			}
			while (begin<end) {
				if(cmp(privot, array[begin])>0) {// 左边元素 < 轴点元素
					begin++;
				}else {// 左边元素 >= 轴点元素
					array[end--] = array[begin];
					break;
				}
				
			}
		}
		// 将轴点元素放入最终的位置
		array[begin] = privot;
		// 返回轴点元素的位置
		return begin;
	}
◼希尔排序(Shell Sort)

◼希尔排序把序列看作是一个矩阵,分成𝑚列,逐列进行排序
✓𝑚从某个整数逐渐减为1
✓当𝑚为1时,整个序列将完全有序
●因此,希尔排序也被称为递减增量排序(Diminishing Increment Sort)
●矩阵的列数取决于步长序列(step sequence)
✓比如,如果步长序列为{1,5,19,41,109,…},就代表依次分成109列、41列、19列、5列、1列进行排序
✓不同的步长序列,执行效率也不同

希尔排序–实例
假设有11个元素,步长序列是{1, 2, 5}
在这里插入图片描述
●假设元素在第col 列、第row 行,步长(总列数)是step
那么这个元素在数组中的索引是col + row * step
比如9 在排序前是第2 列、第0 行,那么它排序前的索引是2 + 0 * 5= 2
比如4 在排序前是第2 列、第1 行,那么它排序前的索引是2 + 1 * 5= 7

Java实现

@Override
	protected void sort() {
		List<Integer> stepSequence = shellStepSequence();
		for (Integer step:stepSequence) {
			sort(step);
		}
	}
	/**
	 * 分成step列进行排序
	 */
	private void sort(int step) {
		// col : 第几列,column的简称
		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;
				}
			}
		}
	}
	//希尔本人给出的步长序列,最坏情况时间复杂度是O(n2)
	private List<Integer> shellStepSequence() {
		List<Integer> stepSequence = new ArrayList<>();
		int step = array.length;
		while ((step >>= 1) > 0) {
			stepSequence.add(step);
		}
		return stepSequence;
	}

希尔排序–步长序列
目前已知的最好的步长序列,最坏情况时间复杂度是O(n4/3)
在这里插入图片描述

private List<Integer> sedgewickStepSequence() {
		List<Integer> stepSequence = new LinkedList<>();
		int k = 0, step = 0;
		while (true) {
			if (k % 2 == 0) {
				int pow = (int) Math.pow(2, k >> 1);
				step = 1 + 9 * (pow * pow - pow);
			} else {
				int pow1 = (int) Math.pow(2, (k - 1) >> 1);
				int pow2 = (int) Math.pow(2, (k + 1) >> 1);
				step = 1 + 8 * pow1 * pow2 - 6 * pow2;
			}
			if (step >= array.length) break;
			stepSequence.add(0, step);
			k++;
		}
		return stepSequence;
	}
◼计数排序(Counting Sort)

●之前学习的冒泡、选择、插入、归并、快速、希尔、堆排序,都是基于比较的排序,平均时间复杂度目前最低是O(nlogn)
●计数排序、桶排序、基数排序,都不是基于比较的排序,它们是典型的用空间换时间,在某些时候,平均时间复杂度可以比Onlogn更低
计数排序于1954年由Harold H. Seward提出,适合对一定范围内的整数进行排序
计数排序的核心思想:统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引
在这里插入图片描述简单实现

protected void sort() {
		// 找出最大值
		int max = array[0];
		for (int i = 0; i < array.length; i++) {
			if (array[i] > max) {
				max = array[i];
			}
		}
		// 开辟内存空间,存储每个整数出现的次数
		int[] counts = new int[1 + max];
		// 统计每个整数出现的次数
		for (int i = 0; i < array.length; i++) {
			counts[array[i]]++;
		}
		// 根据整数的出现次数,对整数进行排序
		int index = 0;
		for (int i = 0; i < counts.length; i++) {
			while (counts[i]-- > 0) {
				array[index++] = i;
			}
		}
	}

计数排序–改进思路
在这里插入图片描述
改进实现

protected void sort() {
		// 找出最值
		int max = array[0];
		int min = array[0];
		for (int i = 1; i < array.length; i++) {
			if (array[i] > max) {
				max = array[i];
			}
			if (array[i] < min) {
				min = array[i];
			}
		}
		// 开辟内存空间,存储次数
		int[] counts = new int[max-min+1];
		// 统计每个整数出现的次数
		for (int i = 0; i < array.length; i++) {
			counts[array[i]-min]++;
		}
		// 累加次数
		for (int i = 1; i < counts.length; i++) {
			counts[i] += counts[i-1];
		}
		// 从后往前遍历元素,将它放到有序数组中的合适位置
		int[] newArray = new int[array.length];
		for(int i =array.length-1;i>=0;i--) {
			newArray[--counts[array[i]-min]] = array[i];
		}
		// 将有序数组赋值到array
		for (int i = 0; i < newArray.length; i++) {
			array[i] = newArray[i];
		}
	}
◼基数排序(Radix Sort)

基数排序非常适合用于整数排序(尤其是非负整数),因此只演示对非负整数进行基数排序
在这里插入图片描述
●执行流程:依次对个位数、十位数、百位数、千位数、万位数…进行排序(从低位到高位)
个位数、十位数、百位数的取值范围都是固定的0~9,可以使用计数排序对它们进行排序

@Override
	protected void sort() {
		// 找出最大值
		int max = array[0];
		for (int i = 1; i < array.length; i++) {
			if (array[i] > max) {
				max = array[i];
			}
		}
		// 个位数: array[i] / 1 % 10 = 3
		// 十位数:array[i] / 10 % 10 = 9
		// 百位数:array[i] / 100 % 10 = 5
		// 千位数:array[i] / 1000 % 10 = ...
		for (int divider = 1; divider <= max; divider *= 10) {
			countingSort(divider);
		}

	}
	protected void countingSort(int divider) {
		// 开辟内存空间,存储次数
		int[] counts = new int[10];
		// 统计每个整数出现的次数
		for (int i = 0; i < array.length; i++) {
			counts[array[i] / divider % 10]++;
		}
		// 累加次数
		for (int i = 1; i < counts.length; i++) {
			counts[i] += counts[i - 1];
		}
		// 从后往前遍历元素,将它放到有序数组中的合适位置
		int[] newArray = new int[array.length];
		for (int i = array.length - 1; i >= 0; i--) {
			newArray[--counts[array[i] / divider % 10]] = array[i];
		}
		// 将有序数组赋值到array
		for (int i = 0; i < newArray.length; i++) {
			array[i] = newArray[i];
		}
	}

基数排序–另一种思路
在这里插入图片描述实现
在这里插入图片描述

桶排序(Bucket Sort)

●执行流程
①创建一定数量的桶(比如用数组、链表作为桶)
②按照一定的规则(不同类型的数据,规则不同),将序列中的元素均匀分配到对应的桶
③分别对每个桶进行单独排序
④将所有非空桶的元素合并成有序序列
●元素在桶中的索引:元素值* 元素数量码
在这里插入图片描述实现
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值