以下是学习恋上数据结构与算法的记录,本要内容是排序
◼初识排序
●什么叫排序?
✓排序前: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)
●执行流程
①创建一定数量的桶(比如用数组、链表作为桶)
②按照一定的规则(不同类型的数据,规则不同),将序列中的元素均匀分配到对应的桶
③分别对每个桶进行单独排序
④将所有非空桶的元素合并成有序序列
●元素在桶中的索引:元素值* 元素数量码
实现