数据结构于算法|十大排序算法基本原理及其实现


文章目录

  • 1. 排序的概念及实现
    • 1.1 概念
  • 2. 基于比较的排序算法
    • 2.1 插入排序
    • 2.2 希尔排序
    • 2.3 选择排序
    • 2.4 堆排序
    • 2.5 冒泡排序
    • 2.6 快速排序
      • 2.6.1 **Hoare 法**
      • 2.6.2 快排优化
      • 2.6.3 挖坑法
      • 2.6.4 前后指针法
      • 2.6.5 总结
      • 2.6.6 快排(非递归)
    • 2.7 归并排序
      • 2.7.1 递归版本
      • 2.7.2 迭代版本
  • 3. 不基于比较的算法
    • 3.1 计数排序
    • 3.2 桶排序
    • 3.3 基数排序
  • 4.总结

1. 排序的概念及实现 1.1 概念 排序: 使一组数据,按照其中某个或者某些关键字的大小,递增或者递减的排列起来的操作
稳定性:在一组存在重复数据的记录中,经过排序算法后,这些相等数据的相对次序没有改变,则称这种算法是稳定的.
内部排序:数据记录在内存中进行排序
外部排序:因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存
2. 基于比较的排序算法 数据结构于算法|十大排序算法基本原理及其实现
文章图片

2.1 插入排序 基本思想:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列
算法步骤:
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

public static void insertSort1(int[] array) { for (int i = 1; i < array.length; i++) { //记录插入的数据 int tmp = array[i]; int j = i - 1; //j位置前的序列已经有序,所以这里从最后开始比较 for (; j >= 0; j--) { //如果这里变成大于等于就变成不稳定的 if (array[j] > tmp) { array[j + 1] = array[j]; } else { break; } } array[j + 1] = tmp; } } //code - 2 public static void insertSort2(int[] array) { int n = array.length; for (int i = 0; i < n; i++) { //寻找元素arr[i]合适位置插入 for (int j = i; j > 0; j--) { if (array[j] < array[j-1]) { swap(array, j, j - 1); } else { break; } } } }private static void swap(int[] array, int a, int b) { int tmp = array[a]; array[a] = array[b]; array[b] = tmp; }

总结:
  1. 外层循环对除了第一个元素之外的所有元素进行移动,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动.
  2. 时间复杂度: 如果元素集合越接近有序,直接插入排序算法的时间效率越高,达到0(N),****最坏情况下是相对逆序,达到0(N*2)
  3. 空间复杂度: O(1)
  4. 稳定性:稳定
2.2 希尔排序 基本思想:
选定一个整数gap,代表数据之间的间隔, 距离相同的数据在同一组,并对每一组内的数据排序,然后把gap缩小,重复比较,直到gap==1时,数据已经有序
算法步骤:
  1. 选择一个gap,每次比较完成后gap/2; 直到gap == 1;
  2. 根据gap, 对距离为gap的数据进行排序.
数据结构于算法|十大排序算法基本原理及其实现
文章图片

public static void shell(int[] arr, int gap){ for (int i = gap; i < arr.length; i++) { int tmp = arr[i]; //与插入排序相似,插入排序的gap等于1,而希尔排序的gap在变化,最终gap会等于1 int j = i - gap; for (; j >= 0; j--) { if (arr[j] > tmp) { arr[j + gap] = arr[j]; } else { break; } } arr[j + gap] = tmp; } } public static void shellSort1(int[] array) { int gap = array.length; //这几次都是预排序,使数组变得比较有序 while (gap > 1) { shell(array, gap); gap = gap >>> 2; } //真正的使数组有序 shell(array, 1); }

总结:
  1. 希尔排序是对插入排序的优化
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果.
  3. 时间复杂度:0(n*1.3) - 0(n * 1.5)
  4. 空间复杂度:0(1)
  5. 稳定性:不稳定
2.3 选择排序 基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完
基本步骤:
  1. 在未排序序列中找到最小(最大)数据放到排序序列的起始位置
  2. 继续从剩余未排序的序列中寻找最小(最大)数据,放到已排序的末尾.
  3. 重复第二步直到所有元素排序完毕.

public static void selectSort(int[] array) { for (int i = 0; i < array.length; i++) { int minIndex = i; for (int j = i + 1; j < array.length; j++) { //minIndex始终保存最小值索引,在比较时其下标会更新. if (array[j] < array[minIndex]) { minIndex = i; } } swap(array, minIndex, i); } }public static void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }

总结:
  1. 无论数据有序或者无序,**时间复杂度都是0(N*2),**导致应用场景很少
  2. 空间复杂度:0(1)
  3. 稳定性: 不稳定
2.4 堆排序 基本思想:
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点 ,分为两种方法.排升序要建大堆,排降序建小堆。,如果不懂堆可以看看这篇文章链接
基本步骤:
  1. 选择升序,建大堆
  2. 把堆顶和堆尾元素交换.
  3. 向下调整为大根堆,
  4. 堆的大小减1,重复2,3操作.
原因:因为我们创建的是大根堆,所以在调整时,最大的元素始终会在堆顶,然后我们交换堆顶和堆尾元素,每一次都使堆中的最大元素到堆尾去,然后减少堆大小,从而最终数据有序化

public static void heapSort(int[] arr) { //建堆 createHeap(arr); int end = arr.length - 1; while (end >= 0) { swap(arr, 0, end); shiftDown(arr, 0, end); end--; } } /** * 向下调整 - * 时间复杂度 0(logN) * @param arr 二叉树中存放的元素 * @param root 根节点的下标 * @param len 数组长度 */ private static void shiftDown(int[] arr, int root, int len) { int parent = root; int child = (2 * parent) + 1; //左孩子 //孩子节点索引不会大于len while (child < len) { //使child索引位置保存子节点的最大值 if (child + 1< len && arr[child] < arr[child + 1]) { child++; } if (arr[child] > arr[parent]) { swap(arr, child, parent); parent = child; child = (2 * parent) + 1; } else { break; } } } /** * 建堆 * 时间复杂度 0(n) * @param arr */ private static void createHeap(int[] arr) { for (int p = (arr.length - 1 - 1) / 2; p >=0; p--) { shiftDown(arr, p, arr.length); } } private static void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }

总结:
  1. 时间复杂度:O(N*logN)
  2. 空间复杂度:O(1)
  3. 稳定性:不稳定
2.5 冒泡排序 基本思想:
根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置
基本步骤:
它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来.
数据结构于算法|十大排序算法基本原理及其实现
文章图片

public static void bubbleSort(int[] arr) { for (int i = 0; i < arr.length - 1; i++) { boolean flag = false; for (int j = 0; j < arr.length - i - 1; j++) { if (arr[j] > arr[j + 1]) { swap(arr, j, j + 1); flag = true; } } if (!flag) { break; } } } private static void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }

总结:
  1. 时间复杂度:最坏0(n*2) 最好0(N)
  2. 空间复杂度:O(1)
  3. 稳定性:稳定
2.6 快速排序
快排是由Tony Hoare于1962年提出的一种二叉树结构的交换排序方法.

基本思想:
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
算法步骤:
  1. 从待排序列中选择一个"基准"(piovt)
  2. 按照基准对待排序列进行排序,小于基准的放到基准前边,大于基准的放到基准后边,如果有重复元素放到任意一边(根据划分函数的逻辑来确定),这个操作叫做分区(partion)
  3. 递归的把剩余子序列进行排序

基本框架:
public static void quickSort(int[] arr) { quick(arr, 0, arr.length- 1); } private static void quick(int[] arr, int left, int right) { if (left >= right) { return; } int pivot = partitionHoare(arr, left, right); //递归排(left, pivot - 1] quick(arr, left, pivot - 1); //递归排排(pivot + 1, right] quick(arr,pivot + 1, right); }

通过观察可以看出其基本框架和二叉树的前序遍历十分相似,大家可以在写递归框架时联想到前序遍历.下面就是分区的方法:
2.6.1 Hoare 法

基本步骤:
  1. 选择关键值key, 一般选择最左边为key
  2. 让左指针找到比key大的位置,右指针找到比key小的位置,然后交换.
  3. 当左右指针相遇时,把key放到属于它的位置.
/** * Hoare法 * 如果是顺序或者逆序,递归退化为单分枝, 为 o(N^2) 平均为 o(N*logN) * @param arr * @param low * @param high * @return */ private static int partitionHoare(int[] arr, int low, int high) { int i = low; int tmp = arr[low]; while (low < high) { //左边找比基准大的 while (low < high && arr[high] >= tmp) { high--; } //右边找比基准小的 while (low < high && arr[low] <= tmp) { low++; } swap(arr, low, high); } //把基准挪到它真正的位置 swap(arr,low,i); //返回新的基准 return low; }

2.6.2 快排优化
三数取中法取key
我们先思考一个问题,如果数据是有序,那么二叉树退化为单分枝,时间复杂度为0(N*2),为了减少比较,我们可以随机取key,但是它比较靠运气,所以就出现了三数取中法,它主要是为了解决排序接近有序或者有序的情况下快速排序的效率将会非常的低,并且递归的深度非常的深导致的问题。
private static int middleOfThreeIndex(int[] arr, int left, int right) { //注意这里符号的优先级,加括号的位置 int mid = left + ((right - left) >>> 1); if (arr[left] < arr[right]) { if (arr[mid] < arr[left]) { return left; } else if (arr[mid] > arr[right]) { return right; } else { return mid; } } else { if (arr[mid] < arr[right]) { return right; } else if (arr[mid] > arr[left]) { return left; }else { return mid; } } }

递归到小的子区间时,考虑使用插入排序
private static void quick(int[] arr, int left, int right) { if (left >= right) { return; } //1.数据越来越接近有序,所以用直接插入排序,速度快,这里的区间根据数据量来设置 if (right - left + 1 <= 7000) { insertSort(arr, left, right); return; } //2.三数取中法 减少递归深度,避免单分枝 int index = middleOfThreeIndex(arr, left, right); swap(arr,left, index); int pivot = partitionHoare(arr, left, right); //递归排(left, pivot - 1] quick(arr, left, pivot - 1); //递归排排(pivot + 1, right] quick(arr,pivot + 1, right); }

2.6.3 挖坑法

基本步骤:
  1. 从第一个数据开始,作为key.
  2. 右指针先寻找比key小的,然后把它放入左坑中,自己形成新的坑位,然后左指针寻找比key大的数据,放到右坑中,自己形成新的坑位.
  3. 重复以上操作,直到左右指针相遇.
  4. 把key放到空坑中
常见问题及解释:
  1. 为什么key选择最左边的值,就要先让右边的数先走先去找小?
这是为了确保最后相遇时array[low] < array[key], 只要让右边的数先走,最后停下来时无论是“左边遇到右边”还是“右边遇到左边”,都满足array[low]
  1. 为什么比较时要取大于等于?
解决重复元素问题,因为在遍历序列中我们找的仅仅是比key大或小的元素,如果等于的话就直接跳过。
private static int partitionHole(int[] arr, int low, int high) { //把key挖出 int tmp = arr[low]; while (low < high) { //右边找比key小的 while (low < high && arr[high] >= tmp) { high--; } //小的放到左坑中,自己形成新的坑 arr[low] = arr[high]; //左边找比key大的 while (low < high && arr[low] <= tmp) { low++; } //现在左坑是小值的,然后把大的放到右边 arr[high] = arr[low]; } //最后,把key放到它真正的位置 arr[low] = tmp; return low; }

2.6.4 前后指针法

基本步骤:
  1. 首先cur指向第一个元素,prev指向cur下一个元素,还是选择首元素作为key.
  2. cur向后寻找比key小的数据,然后停下来
  3. 先++prev 判断是否与cur相等, 防止自己与自己交换
  4. 交换prev和cur下标对于的数据
  5. 直到cur越界,最后交换prev和key的数据
private static int partitionPointer2(int[] array, int low, int high) { int key = low; int prev = low ; int cur = low+1; while (cur <= high) { if(array[cur] < array[key] && ++prev != cur) { swap(array,cur,prev); } cur++; } swap(array,prev,key); return prev; }

总结:
  1. 观察动态图可以看出,当cur没有遇到比key大的元素时,prev就跟其后,cur遇到比key大的元素时,cur就会跑的非常快.
  2. cur一直在前找小于key的值,所以可以得出最终prev索引前的数据一定是小于key的.
  3. 在最后交换prev和key的作用是可以保证a[prev]一定小于a[key],因为2中我们知道prev位置的数据一定小于key,所以在退出循环后,把二者交换,返回新的基准
2.6.5 总结
  1. 时间复杂度:0(N*logN)
  2. 空间复杂度:0(logN) 递归占用的
  3. 由于快排在底层做了很多优化,导致其比具有相同时间复杂度的排序算法要快的许多
2.6.6 快排(非递归)
递归版本会在递归过程中会占用巨大的栈空间,导致栈溢出,所以我们使用栈来模拟实现递归过程
void QuickSortNonR(int[] a, int left, int right) { Stack st = new Stack<>(); st.push(left); st.push(right); while (!st.empty()) { right = st.pop(); left = st.pop(); if(right - left <= 1) continue; int div = partitionHoare(a, left, right); // 以基准值为分割点,形成左右两部分:[left, div) 和 [div+1, right) st.push(div+1); st.push(right); st.push(left); st.push(div); } }

2.7 归并排序 2.7.1 递归版本
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

数据结构于算法|十大排序算法基本原理及其实现
文章图片

private static void merge(int[] array,int low,int mid,int high) { int s1 = low; int e1 = mid; int s2 = mid+1; int e2 = high; int[] tmpArr = new int[high-low+1]; int k = 0; //代表tmpArr的下标 //证明两个段都有数据 while (s1 <= e1 && s2 <= e2) { if(array[s1] <= array[s2]) { tmpArr[k++] = array[s1++]; }else { tmpArr[k++] = array[s2++]; } } //剩余元素,全部挪过来 while (s1 <= e1) { tmpArr[k++] = array[s1++]; } //剩余元素,全部挪过来 while (s2 <= e2) { tmpArr[k++] = array[s2++]; } for (int i = 0; i < tmpArr.length; i++) { array[i+low] = tmpArr[i]; } } private static void mergeSortInternal(int[] array,int low,int high) { if(low >= high) return; int mid = low+((high-low)>>>1); mergeSortInternal(array,low,mid); mergeSortInternal(array,mid+1,high); merge(array,low,mid,high); } public static void mergeSort(int[] array) { mergeSortInternal(array,0,array.length-1); }

总结:
  1. 时间复杂度:时间复杂度:N*logN【和数据是否有序没有关系】
  2. 空间复杂度:O(n)
  3. 稳定性:稳定的排序
2.7.2 迭代版本
public static void mergeSortNor(int[] array) { int gap = 1; while (gap < array.length) { for (int i = 0; i < array.length; i += 2*gap) { int left = i; int mid = left+gap-1; //修正mid if(mid >= array.length) { mid = array.length-1; } int right = mid+gap; //修正right if(right >= array.length) { right = array.length-1; } merge(array,left,mid,right); } gap *= 2; } }

3. 不基于比较的算法 3.1 计数排序 基本思想:
【数据结构于算法|十大排序算法基本原理及其实现】将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
基本步骤:
  1. 获取数组中最大值和最小值来计算出所需的空间
  2. 开始计数
  3. 把排好的数据放会原数组

public static void countSort(int[] array) { //1、获取最大值和最小值 int maxVal = array[0]; int minVal = array[0]; //O(N) for (int i = 1; i < array.length; i++) { if(array[i] < minVal) { minVal = array[i]; } if(array[i] > maxVal) { maxVal = array[i]; } } //2、开始计数 int range = maxVal-minVal+1; int[] count = new int[range]; //O(N) for (int i = 0; i < array.length; i++) { count[array[i]-minVal]++; } //3、遍历这个计数数组,把数据放回array //O(范围 + n), 某一次 可能是n次 有可能一个数重复重新多次 int index = 0; for (int i = 0; i < count.length; i++) { while (count[i] > 0) { array[index++] = i+minVal; count[i]--; } } }

总结:
  1. 时间复杂度:O(范围+N)
  2. 空间复杂度:O(范围)
3.2 桶排序 基本思想:
利用了函数的映射关系,把具有这种关系(在一定范围的元素)的元素放到一个桶中,进行排序,高效与否的关键就在于这个映射函数的确定.
数据结构于算法|十大排序算法基本原理及其实现
文章图片

public static void bucketSort(int[] arr) { // 1. 计算最大值和最小值 int max = arr[0]; int min = arr[0]; for (int x : arr) { max = Math.max(max, x); min = Math.min(min, x); } //2.计算桶的数量 int bucketNum = (max - min) / arr.length + 1; ArrayList> bucketArr = new ArrayList<>(bucketNum); for (int i = 0; i < bucketNum; i++) { bucketArr.add(new ArrayList<>()); }//3.将每一个元素入桶 for (int i = 0; i < arr.length; i++) { int num = (arr[i] - min) / (arr.length); bucketArr.get(num).add(arr[i]); }//4.对每一个桶排序 for (int i = 0; i < bucketArr.size(); i++) { Collections.sort(bucketArr.get(i)); }//把桶中元素重新赋值 int pos = 0; for (int i = 0; i < bucketArr.size(); i++) { for (int j = 0; j < bucketArr.get(i).size(); j++) { arr[pos++] = bucketArr.get(i).get(j); } } }

总结:
  1. 为了使桶排序更加高效,我们应当在额外空间充足的情况下,尽量增大桶的数量; 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
3.3 基数排序 基本思想:
将整数按位数切割成不同的数字,然后按每个位数分别比较

public static void radixSort(int[] arr) { //1.获取基数 - 也就是最大位数 - 决定比较次数 int radix = getRadix(arr); //2.创建桶 list所有桶的集合 每一个桶是LinkedList当成队列来用, //创建0~9个桶(桶的底层是队列),因为所有的数字元素都是由0~9的十个数字组成 LinkedList[] lists = new LinkedList[10]; //初始化 for (int i = 0; i < lists.length; i++) { lists[i] = new LinkedList<>(); } //3.开始 分类-装桶 //依次判断每个元素的个位,十位至MAX位,存入对应的桶中,出队,存入原数组;直至MAX轮结束输出数组。 for (int r = 1; r <= radix; r++) { //分类过程 for (int i = 0; i < arr.length; i++) { lists[getIndex(arr[i], r)].offer(arr[i]); } int index = 0; //放到原数组 for (int i = 0; i < lists.length; i++) { while (!lists[i].isEmpty()) { arr[index++] = lists[i].poll(); } } } }/** * 获取下标 * @param num * @param r * @return */ private static int getIndex(int num, int r) { int ret = 0; for (int i = 1; i <= r; i++) { ret = num % 10; num /= 10; } return ret; }/** * 获取最大位数 * @param arr * @return */ private static int getRadix(int[] arr) { int max = arr[0]; for (int i = 1; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } } return (max + "").length(); }

4.总结 排序算法复杂度及稳定性分析
数据结构于算法|十大排序算法基本原理及其实现
文章图片

数据结构于算法|十大排序算法基本原理及其实现
文章图片

    推荐阅读