排序算法在一些算法 思想中会经常使用到下面就对一些常用的排序算法做一个记录,常见的排序算法可以分为:
0 排序基础知识
0.1 排序分类
比较类排序:通过比较来决定元素的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
非比较类排序:不通过比较来确定元素的次序,可以突破基于比较的时间下界,时间是线性的。因此也叫线性时间非比较排序
0.2 时间空间复杂度。
比较信息等可以参考下图,图片来自牛客网:
0.3 相关概念
稳定:排序结果不受元素之间的相对位置限制,既a在b前面,且a=b,排序后a任然在b前面。
时间复杂度O() 即从序列的初始状态到经过排序算法的变换移位等操作变到最终排序好的结果状态的过程所花费的时间度量。
空间复杂度O() 就是从序列的初始状态经过排序移位变换的过程一直到最终的状态所花费的空间开销。
1、冒泡排序
1.1 冒泡算法介绍
冒泡排序算法是把较小的元素往前调或者把较大的元素往后调。这种方法主要是通过对相邻两个元素进行大小的比较,根据比较结果和算法规则对该二元素的位置进行交换,这样逐个依次进行比较和交换,就能达到排序目的。
1.2 基本思想
冒泡排序的基本思想是,
- 1、首先将第1个和第2个记录的关键字比较大小,如果是逆序的,就将这两个记录进行交换,再对第2个和第3个记录的关键字进行比较,依次类推;
- 2、重复进行上述计算,直至完成第(n一1)个和第n个记录的关键字之间的比较,
- 3、此后,再按照上述过程进行第2次、第3次排序,直至整个序列有序为止。排序过程中要特别注意的是,当相邻两个元素大小一致时,这一步操作就不需要交换位置,因此也说明冒泡排序是一种严格的稳定排序算法,它不改变序列中相同元素之间的相对位置关系。
1.3代码实现
public static int[] BubbleSort(int[] array) {
if(array.length<=1) return array;
for(int i = 0;i<=array.length-1;i++) {
//method1
for(int j=i;j<array.length;j++) {
if(array[i]>array[j]) {
array = swap(array,i,i+1);
}
}
//method2
for(int j = 0;j<array.length-i-1;j++) if(array[j]>array[j+1]) swap(array,j,j+1);
}
return array;
}
private static int[] swap(int[] arr ,int a,int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
return arr;
}
JavaScript实现
function bubbleSort(arr){
let len = arr.length;
for(let i = 0;i<len;i++){
for(let j = i+1;j<len;j++){
let temp = arr[i]
if(arr[j]<arr[i]){
;[arr[i],arr[j]]=[arr[j],arr[i]]
}
}
}
return arr
}
console.log(bubbleSort([7,3,4,5,10,7,8,2]))
经过两次比较,是稳定的排序算法 因此平均、最坏时间复杂度均为O(n^2),最好时间复杂度O(n), 空间复杂度O(1)
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 |
冒泡 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
2选择排序
2.1 基本思想
选择排序算法的基本思路是首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。
2.2 算法描述
- 首先从第1个位置开始对全部元素进行选择,选出全部元素中最小的给该位置,再对第2个位置进行选择,在剩余元素中选择最小的给该位置即可;
- 以此类推,重复进行“最小元素”的选择,直至完成第(n-1)个位置的元素选择,则第n个位置就只剩唯一的最大元素,此时不需再进行选择。
使用这种排序时,要注意其中一个不同于冒泡法的细节。
举例说明:序列58539.我们知道第一遍选择第1个元素“5”会和元素“3”交换,那么原序列中的两个相同元素“5”之间的前后相对顺序就发生了改变。因此,我们说选择排序不是稳定的排序算法,它在计算过程中会破坏稳定性。
2.3 代码实现
public static int[] selectionSort(int[] arr) {
int len = arr.length;
int minPoint,temp;
for(int i = 0;i<len-1;i++) {
minPoint = i;
for(int j = i+1;j<len;j++) {
if(arr[j]<arr[minPoint]) { //找未排序部分的最小数
minPoint = j; //交换最小的指针
}
//开始交换
temp = arr[i];
arr[i] = arr[minPoint];
arr[minPoint] = temp;
}
}
return arr;
}
public static void main(String[] args) {
int[] arr = {1,2,5,8,7,6,10,15,16};
int[] result = selectionSort(arr);
for(int a : result) {
System.out.println(a+" ");
}
}
JavaScript实现
function selectionSort(arr){
let len = arr.length;
if(len<=1) return arr;
let minPoint;
for(let i=0;i<len-1;i++){
minPoint = i;
for(let j=i+1;j<len;j++){
if(arr[j]<arr[minPoint]){
minPoint = j;
}
}
[arr[minPoint],arr[i]] = [arr[i],arr[minPoint]]; //ES6 解构赋值 可以参考我的博客
}
return arr;
}
var arr = [1,5,8,6,8,7,9,12,36,15];
console.log(selectionSort(arr))
2.4 算法分析
选择排序的结果不稳定,任何情况下时间复杂度永远是O(n^2),所以数量较小时速度处理较快,但是空间复杂度较小
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
不稳定可以在排序数组中添加两个相同的数字,出现以下结果
3 插入排序
3.1 基本思想
插入排序算法是基于某序列已经有序排列的情况下,通过一次插入一个元素的方式按照原有排序方式增加元素。
3.2 算法描述
这种比较是从该有序序列的最末端开始执行,
- 1、要插入序列中的元素最先和有序序列中最大的元素比较(默认第一个元素已经排序好,从第一个元素开始),
- 2、若其大于该最大元素,则可直接插入最大元素的后面即可,否则再向前一位比较查找直至找到应该插入的位置为止。
- 插入排序的基本思想是,每次将1个待排序的记录按其关键字大小插入到前面已经排好序的子序列中,寻找最适当的位置,直至全部记录插入完毕。执行过程中,若遇到和插入元素相等的位置,则将要插人的元素放在该相等元素的后面,因此插入该元素后并未改变原序列的前后顺序。
我们认为插入排序也是一种稳定的排序方法。插入排序分直接插入排序、折半插入排序和希尔排序3类
3.3 算法实现
//method1
private static int[] DirectInsertSort(int[] array) {
if(array.length<=1) return array;
for(int i=0;i<array.length;i++) {
int temp = array[i];
int j = i ;
while(j>0&&temp<array[j-1]) { //当 i=0时 不执行
array[j] = array[j-1]; //将大数右移动
j--; // 遍历已经排序的数组
}
array[j]=temp; //将移出的空位填上小数
}
return array;
}
JavaScript实现
//插人排序
//基本思想:在序号i之前的元素(0到i-1)已经排好序,
//本趟需要找到i对应的元素x (此时即arr[i]) 的正确位置k,
//在寻找位置k的过程中与序号i-1到0的元素依次进行比较。
//如果x小于比较元素,则比较元素向后移动一位;否则,结束移位,
//将x插入当前位置k
function insertSort(arr){
let len = arr.length;
if(arr.length<=0) return ;
for(let i = 1;i<arr.length;i++){
//将待插入元素提取出来
let temp = arr[i];
let j ;
for(j = i-1;j>=0;j--){
if(arr[j]>temp){
//插入元素小于比较元素 比较元素则向后移动一位
arr[j+1]=arr[j];
}else{
//否则结束移位
break;
}
}
arr[j+1]=temp;
}
return arr;
}
console.log(insertSort([7,3,4,5,10,7,8,2,9,]))
3.4 算法分析
插入排序采用in-palce排序(可以理解为不需要辅助数组,原地排序)
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 |
插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
4、希尔排序
4.1 基本思想
在要排序的一组数中,根据某一增量分为若干子序列,并对子序列分别进行插入排序。
然后逐渐将增量减小,并重复上述过程。直至增量为1,此时数据序列基本有序,最后进行插入排序。
4.2 算法描述
将待排序的序列分割成若干个子序列分别进行直接插入排序
- 选择增量序列:K1,K2,...,Kn,其中Ki>Kj,Kn=1;
- 按增量序列个数n,对序列进行kn趟排序;
- 每趟排序,根据对应的增量Ki,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
4.3 算法实现
public int[] shellSort(int[] arr){
int len = arr.length;
int step ;
for(step = len/2;step>0;step/=2){ //基数为2的希尔排序
for(int i =step;i<len;i++){
int j = i;
int temp = arr[j];
while(j>step-1&&temp<arr[j-step]){
arr[j]=arr[j-temp];
j-=temp;
}
arr[j]=temp;
}
}
return arr;
}
///method2 基数为3
public static int[] ShellKnuthSort(int[] str) {
int step = 1;
int len = str.length;
while(step<=len/3) {
step = step*3+1;
}
while(step>0){
//先根据增量排序
for(int i = 0;i<len;i++) {
int temp = str[i];
int j = i;
//交换大小1
while(j>step-1&&temp<=str[j-step]) {
str[j]=str[j-step]; //temp小于 前面位数上的值 做交换
j-=step; //步长之间比较
}
str[j] = temp;
//交换大小2
// while(j>step-1&&temp<str[j-step]) {
// str = swap(str,j,j-step);
// j-=step;
// }
}
step = (step-1)/3;
}
return str;
}
JavaScript实现
//希尔排序
function shellSort(arr){
let d = arr.length
while(true){
d = Math.floor(d/2)
for(let x = 0;x<d;x++){
for(let i = x+d;i<arr.length;i=i+d){
let temp = arr[i]
let j
for(j=i-d;j>=0&&arr[j]>temp;j=j-d){
arr[j+d] = arr[j];
}
arr[j+d] = temp;
}
}
if(d==1){
break;
}
}
return arr
}
console.log(shellSort([7, 3, 4, 5, 10, 7, 8, 2]))
4.4 算法分析
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 |
希尔排序 | O(n^1.3) | O(n^2) | O(n) | O(1) | 不稳定 |
5 归并排序
5.1 基本思想
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。
首先考虑下如何将2个有序数列合并。这个非常简单,只要从比较2个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
5.2 算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
5.3 代码实现
/**
* 归并排序
*
* @param array
* @return
*/
public static int[] MergeSort(int[] array) {
if (array.length < 2) return array;
int mid = array.length / 2;
int[] left = Arrays.copyOfRange(array, 0, mid);
int[] right = Arrays.copyOfRange(array, mid, array.length);
return merge(MergeSort(left), MergeSort(right));
}
/**
* 归并排序——将两段有序数组结合成一个有序数组
*
* @param left
* @param right
* @return
*/
public static int[] merge(int[] left, int[] right) {
int[] result = new int[left.length + right.length];
int i = 0,j = 0,k = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
result[k++] = left[i++];
} else {
result[k++] = right[j++];
}
}
while (i < left.length) {
result[k++] = left[i++];
}
while (j < right.length) {
result[k++] = right[j++];
}
return result;
}
JavaScript实现
function merge(left,right){
let result = []
while(left.length>0&&right.length>0){
if(left[0]<right[0]){
/*shift()方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。*/
result.push(left.shift())
}else{
result.push(right.shift())
}
}
return result.concat(left).concat(right)
}
function mergeSort(arr){
if(arr.length == 1) {return arr}
let middle = Math.floor(arr.length/2),
left = arr.slice(0,middle),
right = arr.slice(middle)
return merge(mergeSort(left),mergeSort(right))
}
console.log(mergeSort([7, 3, 4, 5, 10, 7, 8, 2]))
5.4 算法分析
归并排序的形式就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的可以得出它在任何情况下时间复杂度均是O(nlogn)。
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
--------2019.8.20 未完待续
6 快速排序
6.1 基本思想
依次比较然后交换参数位置
- 选择基准值(base),原数组长度减一(基准值),使用 splice
- 循环原数组,小的放左边(left数组),大的放右边(right数组);
- concat(left, base, right)
- 递归继续排序 left 与 right
6.2代码实现
function quickSort(arr){
if(arr.length<=1){
return arr;
}
var left = [],right=[],current=arr.splice(0,1);
for(let i=0;i<arr.length;i++){
if(arr[i]<current){
left.push(arr[i]); //放在左边
}else{
right.push(arr[i]);//放在右边
}
}
return quickSort(left).concat(current,quickSort(right));
}
6.3 算法分析
交换位置,最坏的情况退化为冒泡排序
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(n) | 不稳定 |
7 堆排序
7.1 基本思想
堆排序也是一种很高效的算法,因其把数组当作二叉树来排序而得名。这个算法会根据以下信息,把数组当作二叉树来管理。
* 索引0是树的根节点;
* 除根节点外,任意节点N的父节点是N/2;
* 节点L的左子节点是2*L;
* 节点R的右子节点是2*R+1。
7.2 代码实现
this.heapSort = function(){
var heapSize = array.length;
buildTree(array); //{1}
while(heapSize>1){
heapSize--;
swap(array,0,heapSize); //{2}
heapify(array,heapSize,0); //{3}
}
};
var swap = function(array,index1,index2){
var aux = array[index1];
array[index1] = array[index2];
array[index2] = aux;
};
```
第一步,构造一个满足array[parent(i)] ≥ array[i]的堆结构(行{1})。
第二步,交换堆里第一个元素(数组中较大的值)和最后一个元素的位置(行{2})。这样,最大的值就会出现在它已排序的位置。
第二步可能会丢掉堆的属性。因此,我们还需要执行一个heapify函数,再次将数组转换成堆,也就是说,它会找到当前堆的根节点(较小的值),重新放到树的底部。
``` js
var buildHeap = function(array){
var heapSize = array.length;
for(let i=Math.floor(array.length/2);i>=0;i--){
heapify(array,heapSize,1);
}
};
var heapify = function(array){
var left = i*2+1,
right = i*2+2,
largest = i;
if(left<heapSize&&array[left]>array[largest]){
largest = left;
}
if(right<heapSize&&array[right]>array[left]){
largest = right;
}
if(largest !==i){
swap(array,i,largest);
heapify(array,heapSize,largest);
}
};
7.3 算法分析
二叉树的操作
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
参考链接
1、排序算法-百度百科 https://baike.baidu.com/item/%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/5399605?fr=aladdin
3、菜鸟教程排序算法