文章目录
0.1算法复杂度表
0.2相关概念
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
关于稳定性的意义:
如果我问你:排序算法的「稳定性」有何意义?你怎么回答?
0.3动图演示
1.冒泡排序
排序时相邻两对元素进行比较,从第一对进行到最后一对,较大的元素交换到后方,因为大数像气泡一样从前往后移动,所以叫冒泡排序。
1.1算法描述
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个(最后一个数已经是上一次排序后最大的数);
- 重复步骤1~3,直到排序完成。
1.2代码实现
function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len - 1; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j+1]) { // 相邻元素两两对比
var temp = arr[j+1]; // 元素交换
arr[j+1] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
2.选择排序
在每次排序后,在未排序元素中选出最小的元素,将它放在已排序元素的后边。一开始全是未排序元素,每次循环将一个最小元素放在前方,因此每次循环开始时前i个元素以排序。
选择排序是较稳定的排序,无论参数怎样改变,时间复杂度都是O(n^2),不像其他排序算法一样有好坏情况(归并排序等也具有此特性)。
2.2代码实现
function selectionSort(arr) {
var len = arr.length;
var minIndex, temp;
for (var i = 0; i < len - 1; i++) {
minIndex = i;
for (var j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) { // 寻找最小的数
minIndex = j; // 将最小数的索引保存
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
return arr;
}
3.插入排序
通过构建已排序序列,对于未排序的元素,将其从已排序序列的尾部向前扫描,将其插入到对应的序列从而不断扩大已排序序列。
3.1算法描述
1.从第一个元素开始,该元素可以认为已经被排序;
2.取出下一个元素,在已经排序的元素序列中从后向前扫描;
3.如果该元素(已排序)大于新元素,将该元素移到下一位置;
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
5.将新元素插入到该位置后;
6.重复步骤2~5。
3.2代码实现
function insertionSort(arr) {
var len = arr.length;
var preIndex, current;
for (var i = 1; i < len; i++) {
preIndex = i - 1;
current = arr[i];
while (preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
arr[preIndex + 1] = current;
}
return arr;
}
4.归并排序
该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
4.1算法描述
1.把长度为n的输入序列分成两个长度为n/2的子序列;
2.对这两个子序列分别采用归并排序;
3.将两个排序好的子序列合并成一个最终的排序序列。
通过递归不断进行此过程,递归到最后只剩下两个元素间或者一个元素的归并。
4.2算法分析
分治思想的使用在于sort方法通过递归实现了将原数组的分解,merge算法通过被sort在递归中一次次调用又将数组 排序后合了起来。
merge方法设计的时候是按照一个数组mid的左右两部分是排好序的的情况设计的,因为merge在sort中被递归调用,在递归的最后最深处左右两个数(或一个数)可以看作是已排好序的,然后递归一直返回的数组左右两边就都是排好序的了。
sort()方法在设计的时候引入了mid,从而实现将数组分成两个的情况。
对于时间复杂度:O(nlogn),代表的是执行了logn次n,因为递归每次都将数组分成两半,所以要执行logn次,每次都要进行merge而merge的复杂度是n,因此执行logn次n,即为nlogn。
代码具体讲解:马士兵说:归并排序,java对象排序的默认算法
以及:马士兵说:归并排序(二),程序是调出来的,请大家一定要学会修复BUG
4.3代码实现
作者:优课达
链接:https://zhuanlan.zhihu.com/p/127843825
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
public static void mergeSort(int[] array) {
if (array == null || array.length <= 1) {
return;
}
sort(array, 0, array.length - 1);
}
private static void sort(int[] array, int left, int right) {
if (left == right) {
return;
}
int mid = left + ((right - left) >> 1);
// 对左侧子序列进行递归排序
sort(array, left, mid);
// 对右侧子序列进行递归排序
sort(array, mid + 1, right);
// 合并
merge(array, left, mid, right);
}
private static void merge(int[] array, int left, int mid, int right) {
int[] temp = new int[right - left + 1];
int i = 0;
int p1 = left;
int p2 = mid + 1;
// 比较左右两部分的元素,哪个小,把那个元素填入temp中
while (p1 <= mid && p2 <= right) {
temp[i++] = array[p1] < array[p2] ? array[p1++] : array[p2++];
}
// 上面的循环退出后,把剩余的元素依次填入到temp中
// 以下两个while只有一个会执行
while (p1 <= mid) {
temp[i++] = array[p1++];
}
while (p2 <= right) {
temp[i++] = array[p2++];
}
// 把最终的排序的结果复制给原数组
for (i = 0; i < temp.length; i++) {
array[left + i] = temp[i];
}
}
5.快速排序
通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
5.1算法描述
1.从数列中挑出一个元素,称为 “基准”(pivot);
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
5.3算法分析
这里假设首先将数组的第一个元素当做基准数,定义两个指针i和j,j从尾往前找比基准数小的数,找到了就停下;i从头往后找比基准数大的数,找到了就停下。当两个指针都停下后,交换i和j位置上的值,然后继续移动i和j直到二者相遇。相遇后就交换基准数和相遇位置上的数,从而做到基准数左边的数都比他小,右边的数都比他大。然后递归即可。
代码具体讲解:java快速排序讲解
5.4代码实现
public static void quickSort(int[] arr, int left, int rigth) {
if(left>rigth) {
return;
}
//这里是将left和right间的随机数作为基数,否则的话最坏情况下递归树将退化为链表,时间复杂度是O(n),可加可不加
if (right > left) { //获取随机数作为基数,left和right相等的时候不用
int randomIndex = left+(int) (Math.random()*(right-left+1));
swap(nums, left, randomIndex);
}
int base = arr[left];//定义保存基准数
int i = left;//定义变量i指向最左边
int j = rigth;//定义变量j指向最右边
while(i != j ) { //i和j不相遇时,在循环中继续检索
while(arr[j] >= base && i<j) {
j--;//j从右往左移动
}
while(arr[i] <= base && i<j) {
i++;//i从左往右移动
}
//代码走到这,i和j都停下了,这时候交换i和j位置的元素
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//代码走到这跳出循环,说明i和j相等了,交换相遇位置和基准数的数值
arr[left] = arr[i];
arr[i] = base;
//这里基准数左边的数字都比它小,右边的数字都比他大
//排基准数的左边
quickSort(arr, left, i - 1);
//排基准数的右边
quickSort(arr, j+1, rigth);
}
6.桶排序
1.算法描述
1.将待排序的序列分到若干个桶中,每个桶内的元素再进行个别排序。
2.时间复杂度最好可能是线性O(n),桶排序不是基于比较的排序
2.算法分析
桶排序的算法时间复杂度有两部分组成:
1.遍历处理每个元素,O(n)时间下遍历一遍每个元素
2.每个桶内再次排序的时间复杂度总和
如果桶内元素分配较为均匀假设每个桶内部使用的排序算法为快速排序,那么每个桶内的时间复杂度为(n/m) log(n/m)。有m个桶,那么时间复杂度为m * (n/m)log(n/m)=n (log n-log m).所以最终桶排序的时间复杂度为:O(n)+O(n(log n- log m))=
O(n+n(log n -log m)) 其中m为桶的个数。我们有时也会写成O(n+c),其中c=n*(log n -log m);
使用时要注意:
注意元素分布尽量均匀,桶的个数也需要经过设计。
3.代码实现
作者:bigsai
链接:https://zhuanlan.zhihu.com/p/164992268
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
import java.util.ArrayList;
import java.util.List;
//微信公众号:bigsai
public class test3 {
public static void main(String[] args) {
int a[]= {1,8,7,44,42,46,38,34,33,17,15,16,27,28,24};
List[] buckets=new ArrayList[5];
for(int i=0;i<buckets.length;i++)//初始化
{
buckets[i]=new ArrayList<Integer>();
}
for(int i=0;i<a.length;i++)//将待排序序列放入对应桶中
{
int index=a[i]/10;//对应的桶号
buckets[index].add(a[i]);
}
for(int i=0;i<buckets.length;i++)//每个桶内进行排序(使用系统自带快排)
{
buckets[i].sort(null);
for(int j=0;j<buckets[i].size();j++)//顺便打印输出
{
System.out.print(buckets[i].get(j)+" ");
}
}
}
}
7.基数排序
1.算法描述
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
2.算法分析
第一步
以LSD为例,假设原来有一串数值如下所示:
73, 22, 93, 43, 55, 14, 28, 65, 39, 81
首先根据个位数的数值,在走访数值时将它们分配至编号0到9的桶子中:
0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39
第二步
接下来将这些桶子中的数值重新串接起来,成为以下的数列:
81, 22, 73, 93, 43, 14, 55, 65, 28, 39
接着再进行一次分配,这次是根据十位数来分配:
0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93
第三步
接下来将这些桶子中的数值重新串接起来,成为以下的数列:
14, 22, 28, 39, 43, 55, 65, 73, 81, 93
这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。
LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好。MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。
3.代码实现
public void maximumGap(int[] nums) {
int n = nums.length;
long exp = 1;//从低位到高位
int[] buf = new int[n];//排序后的数存在buf中
int maxVal = Arrays.stream(nums).max().getAsInt();
// int最大有10位数也就是十亿以上,所以可能会循环10次以上
while (maxVal >= exp) {
int[] cnt = new int[10];//记录每个数应该放置在buf中的位置
for (int i = 0; i < n; i++) {// 这里是O(n)
int digit = (nums[i] / (int) exp) % 10;
cnt[digit]++;
}
for (int i = 1; i < 10; i++) {
cnt[i] += cnt[i - 1];//全部加完之后就得到位置信息了。digit越大排的越往后
}
for (int i = n - 1; i >= 0; i--) {// 这里是O(n)
int digit = (nums[i] / (int) exp) % 10;
buf[cnt[digit] - 1] = nums[i];//根据位置信息放置数
cnt[digit]--;
}
System.arraycopy(buf, 0, nums, 0, n);// 这里是O(n),数字分别是开始的位置、开始的位置和长度
exp *= 10;
}
}
8.堆排序
1.算法描述
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。
堆
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
堆排序的基本思想是:
将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
2.算法分析
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
时间复杂度:O(NlogN):建堆和for循环都是NlogN,siftDown一次是logN,循环N次
空间复杂度:O(1)
3.代码实现
public int[] sortArray(int[] nums){
int size = nums.length;
buildHeap(nums,size);
for(int j=nums.length-1;j>0;j--){
swap(nums,0,j);
siftDown(0,nums[0],j,nums);
}
return nums;
}
void buildHeap(int[] queue,int size){ //从最后一个非叶子节点开始构建大顶堆
for(int i=size/2-1;i>=0;i--){
siftDown(i,queue[i],size,queue);
}
}
//这里siftDown是使用的PrioeityQueue的源码
void siftDown(int k,int x,int size,int[] queue) {
int key = x; //当前节点的值
int half = size >>> 1; // loop while a non-leaf 非叶子节点
while (k < half) { //保持在非子叶节点内
int child = (k << 1) + 1; // assume left child is least 获取左孩子节点所在的位置
int c = queue[child]; // 获取左孩子节点元素值
int right = child + 1; // 右孩子节点所在位置
if (right < size && c < queue[right]) {
c = queue[child = right]; // 记录左右孩子中最大的元素,如果右边比左边大就记录右边的值并将right赋值给child
}
if (key >= c) // 如果父节点比两个孩子节点都要大,就结束循环
break;
queue[k] = c; // 把小的元素移到父节点的位置
k = child; // 记录孩子节点所在的位置,继续向下调整
}
queue[k] = key; // 最终把父节点放在对应的位置上,使其保持一个小顶堆
}
void swap(int[] queue,int a,int b){
int temp = queue[a];
queue[a] = queue[b];
queue[b] = temp;
}