常见排序算法总结与实现
排序
1 冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较相邻两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
1.1 算法描述
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
文章图片
1.2 算法分析 时间复杂度:T(n) = O(n2) 最好情况O(n) 最坏情况O(n2)
空间复杂度:O(1)
稳定性:稳定
1.3 算法实现
public static void sort(int[] arr) {
if(arr == null || arr.length < 2) return ;
int n = arr.length - 1;
for (int i = 0;
i < n;
i++) {
// 每次冒泡前都要置为false
boolean swap = false;
for (int j = 0;
j < n - i;
j++) {
if(arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
swap = true;
}
}
if(!swap) break;
System.out.println("排序中:" + Arrays.toString(arr));
}
}
2 选择排序
表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
2.1 算法描述 n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
- 初始状态:无序区为R[1…n],有序区为空;
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- n-1趟结束,数组有序化了。
文章图片
2.2 算法分析 时间复杂度:平均情况 O(n2) 最好情况 O(n^2) 最坏情况 O(n^2)
空间复杂度: O(1)
稳定性:不稳定
2.3 算法实现
public static void sort(int[] arr) {
if (arr == null || arr.length < 2) return;
for (int i = 0;
i < arr.length - 1;
i++) {
int minIndex = i;
// 无序区第一个元素
for (int j = i + 1;
j < arr.length;
j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 交换元素
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
System.out.println(Arrays.toString(arr));
}
}
3 插入排序
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
3.1 算法描述 一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5
文章图片
3.2 算法分析 时间复杂度:平均情况:O(n2) 最好情况 O(n) 最坏情况 O(n^2)
空间复杂度: O(1)
稳定性:稳定
3.3 算法实现
public static void sort(int[] arr) {
if (arr == null || arr.length < 2) return;
for (int i = 1;
i < arr.length;
i++) {
int value = https://www.it610.com/article/arr[i];
int j = i - 1;
while (j>= 0 && arr[j] > value) {
arr[j + 1] = arr[j];
j--;
}
// value 应该插入到 j+1 的索引位置
arr[j + 1] = value;
System.out.println(Arrays.toString(arr));
}
}
4 希尔排序
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。
希尔排序是把元素按一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的元素越来越多,当增量减至1时,整个数组被分成一组,算法便终止。
4.1 算法描述 我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
文章图片
4.2 算法分析 时间复杂度:O(nlogn)
空间复杂度:O(1)
稳定性:不稳定
4.3 算法实现
public static void sort(int[] arr) {
if (arr == null || arr.length < 2) return;
int gap = arr.length / 2;
while (gap != 0) {
// 组间进行直接插入排序
for (int i = gap;
i < arr.length;
i++) {
int value = https://www.it610.com/article/arr[i];
int j = i - gap;
while (j>= 0 && arr[j] > value) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = value;
}
// 缩小增量
gap /= 2;
System.out.println(Arrays.toString(arr));
}
}
5 归并排序
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
5.1 算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
文章图片
5.2 算法分析 最佳情况:T(n) = O(nlogn)
空间复杂度: O(n)
稳定性: 稳定
5.3 算法实现
public static void sort(int[] arr) {
if (arr == null || arr.length < 2) return;
mergeSort(arr, 0, arr.length - 1);
}private static void mergeSort(int[] arr, int left, int right) {
if (left >= right) return;
int mid = left + ((right - left) >> 1);
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
// 合并有序子序列
merge(arr, left, mid, right);
System.out.println(Arrays.toString(arr));
}private static void merge(int[] arr, int left, int mid, int right) {
//[left, mid], [mid+1, right]
int[] temp = new int[right - left + 1];
int i = left;
int j = mid + 1;
int k = 0;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
while(i <= mid) {
temp[k++] = arr[i++];
}
while(j <= right) {
temp[k++] = arr[j++];
}
// 将 temp 数组赋值给对应的区间
for (int x = 0;
x < temp.length;
x++) {
arr[left + x] = temp[x];
}
}
6 快速排序
快速排序的基本思想:通过一趟排序将待排数组分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
6.1 算法描述 快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
文章图片
6.2 算法分析 最佳情况:平均情况:T(n) = O(nlogn)
空间复杂度: O(logn) (栈占用的空间)
稳定性: 不稳定
6.3 算法实现
// Partition
public static int partition(int[] arr, int len, int start, int end) {
// 非法参数处理
if (arr == null || len <= 0 || start < 0 || end >= len)
throw new IllegalArgumentException("Invalid Parameters");
// 加入随机数,对于所有输入情况都能获得较好的期望性能
Random random = new Random();
int r = random.nextInt(Math.abs(end - start)) + start;
// 互换任意元素和最后一个元素,减少移动,最后一个元素为x
swap(arr, r, end);
int index = start - 1;
// 指针,最后x插入的位置// 遍历arr
for (int i = start;
i <= end;
i++) {
if (arr[i] < arr[end]) {
index++;
// 把小于x的元素移动到index左边
if (index != i) swap(arr, index, i);
}// arr[i] > x 不需要考虑
}
// x插入index后,位置互换
swap(arr, ++index, end);
return index;
}private static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}// Quick Sort
public static void quickSort(int[] arr, int len, int start, int end) {
// 递归出口
if (start == end) return;
int idx = partition(arr, len, start, end);
if (idx > start) quickSort(arr, len, start, idx - 1);
if (idx < end) quickSort(arr, len, idx + 1, end);
}
7 堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
文章图片
7.1 算法描述
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
文章图片
7.2 算法分析 最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(nlogn) 平均情况:T(n) = O(nlogn)
空间复杂度:O(1)
稳定性:不稳定
7.3 算法实现 堆
public class Heap {
private int[] elements;
private int size;
public Heap() {
// 索引为0的位置不存储元素
elements = new int[11];
}public Heap(int initialCapacity) {
this.elements = elements;
if (initialCapacity < 0) {
throw new IllegalArgumentException("initialCapacity=" + initialCapacity);
}
elements = new int[initialCapacity + 1];
}public void add(int e) {
if (size == elements.length - 1) return;
elements[++size] = e;
// 从下往上堆化
int i = size;
while (i > 1 && elements[i] > elements[i / 2]) {
swap(elements, i, i / 2);
i = i / 2;
}
}/**
* 删除堆顶的元素
*
* @return 堆顶元素的值
*/
public int remove() {
if (isEmpty()) throw new NoSuchElementException();
int removeValue = https://www.it610.com/article/elements[1];
swap(elements, 1, size);
size--;
// 从上往下堆化
int i = 1;
while (true) {
int maxIndex = i;
int leftChild = 2 * i;
int rightChild = 2 * i + 1;
if (leftChild <= size && elements[leftChild]> elements[maxIndex]) {
maxIndex = leftChild;
}
if (rightChild <= size && elements[rightChild] > elements[maxIndex]) {
maxIndex = rightChild;
}
if(maxIndex == i) break;
swap(elements, i, maxIndex);
i = maxIndex;
}
return removeValue;
}public boolean isEmpty() {
return size == 0;
}private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}@Override
public String toString() {
StringBuilder sb = new StringBuilder("[");
for (int i = 1;
i <= size;
i++) {
if(i != 1) {
sb.append(", ");
}
sb.append(elements[i]);
}
return sb.append("]").toString();
}
}
堆排序
public static void sort(int[] arr) {
if (arr == null || arr.length < 2) return;
// 构建大顶堆
// O(n)
buildHeap(arr);
System.out.println(Arrays.toString(arr));
int len = arr.length;
// len = n
// O(nlog(n))
while (len > 1) {
swap(arr, 0, len - 1);
len--;
heapify(arr, 0, len);
System.out.println(Arrays.toString(arr));
}
}private static void buildHeap(int[] arr) {
// arr.length - 1
// 2 * i + 1 < arr.length
// i <= (arr.length - 2) / 2
for (int i = (arr.length - 2) / 2 ;
i >=0 ;
i--) {
heapify(arr, i, arr.length);
}
}// 从上往下堆化
private static void heapify(int[] arr, int i, int len) {
while (i < len) {
int maxIndex = i;
int leftChid = 2 * i + 1;
int rightChild = 2 * i + 2;
if(leftChid < len && arr[leftChid] > arr[maxIndex]) {
maxIndex = leftChid;
}
if(rightChild < len && arr[rightChild] > arr[maxIndex]) {
maxIndex = rightChild;
}
if(maxIndex == i) break;
swap(arr, i, maxIndex);
i = maxIndex;
}
}private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
8 总结
文章图片
【常见排序算法总结与实现】rightChild < len && arr[rightChild] > arr[maxIndex]) {
maxIndex = rightChild;
}
if(maxIndex == i) break;
swap(arr, i, maxIndex);
i = maxIndex;
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
### 8总结
文章图片
![image-20200513211713077](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9naXRlZS5jb20vaHlwbm9zX3gvZmlndXJlX2JlZC9yYXcvbWFzdGVyL2ltZy8yMDIwMDUxMzIxMTcxNS5wbmc?x-oss-process=image/format,png)
推荐阅读
- 画解算法(1.|画解算法:1. 两数之和)
- Guava|Guava RateLimiter与限流算法
- 唱歌教学(导致嗓音损坏的几个常见的错误唱歌方法!)
- Hive常见问题汇总
- 一个选择排序算法
- SG平滑轨迹算法的原理和实现
- 《算法》-图[有向图]
- 排序(归并排序)
- JS常见数组操作补充
- LeetCode算法题-11.|LeetCode算法题-11. 盛最多水的容器(Swift)