一. 冒泡排序(BubbleSort)
- 基本思想:两个数比较大小,较大的数下沉,较小的数冒起来。
- 过程:
- 比较相邻的两个数据,如果第二个数小,就交换位置。
- 从后向前两两比较,一直到比较最前两个数据。最终最小数被交换到起始的位置,这样第一个最小数的位置就排好了。
-
继续重复上述过程,依次将第2.3...n-1个最小数排好位置。
- 平均时间复杂度:O(n2)
- java代码实现:
其实冒泡排序,外层执行n次循环,内层执行n-1-i次循环,n是元素个数
将最小的数字移动到最前面,从最后面开始移动
public static void BubbleSort(int [] arr){
int temp;//临时变量
for(int i=0; ii; j--){
if(arr[j] < arr[j-1]){
temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
}
}
}
}
}
//冒泡排序实现-将大的数字移动到最后面
for(int i=0;ia[j+1]){
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
1. 优化:
- 针对问题: 数据的顺序排好之后,冒泡算法仍然会继续进行下一轮的比较,直到arr.length-1次,后面的比较没有意义的。
- 方案: 设置标志位flag,如果发生了交换flag设置为true;如果没有交换就设置为false。 这样当一轮比较结束后如果flag仍为false,即:这一轮没有发生交换,说明数据的顺序已经排好,没有必要继续进行下去。
public static void BubbleSort1(int [] arr){
int temp;//临时变量
boolean flag;//是否交换的标志
for(int i=0; ii; j--){
if(arr[j] < arr[j-1]){
temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
flag = true;
}
}
if(!flag) break;
}
}
}
二. 选择排序(SelctionSort)
基本思想: 在长度为N的无序数组中,第一次遍历n-1个数,找到最小的数值与第一个元素交换; 第二次遍历n-2个数,找到最小的数值与第二个元素交换; 。。。 第n-1次遍历,找到最小的数值与第n-1个元素交换,排序完成。
-
过程:
平均时间复杂度:O(n2)
java代码实现:
public static void select_sort(int array[],int length){
for(int i=0;i
三. 插入排序(Insertion Sort)
- 基本思想: 在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。
插入排序,其实就是一直进行两两相连元素的比较交换,最外层进行n次,内层依次进行1次,2次。。。n-1次 -
过程:
相同的场景
- 平均时间复杂度:O(n2)
- java代码实现:
public static void insert_sort(int array[],int lenth){
int temp;
for(int i=0;i0;j--){
if(array[j] < array[j-1]){
temp = array[j-1];
array[j-1] = array[j];
array[j] = temp;
}else{ //不需要交换
break;
}
}
}
}
从大到小排序,与上面只有if判断执行体不同
public static void insertSort(int[] array){
int temp;
for (int i=0;i0;j--){
if (array[j]>array[j-1]){
temp = array[j-1];
array[j-1] = array[j];
array[j] = temp;
} else {
break;
}
}
}
}
四. 希尔排序(Shell Sort)
前言: 数据序列1: 13-17-20-42-28 利用插入排序,13-17-20-28-42. Number of swap:1; 数据序列2: 13-17-20-42-14 利用插入排序,13-14-17-20-42. Number of swap:3; 如果数据序列基本有序,使用插入排序会更加高效。
基本思想: 在要排序的一组数中,根据某一增量分为若干子序列,并对子序列分别进行插入排序。 然后逐渐将增量减小,并重复上述过程。直至增量为1,此时数据序列基本有序,最后进行插入排序。
-
过程:
希尔排序
- 平均时间复杂度:O(NlogN)
- java代码实现:
public static void shell_sort(int array[],int lenth){
int temp = 0;
int incre = lenth;
while(true){
incre = incre/2;
for(int k = 0;kk;j-=incre){
if(array[j]
/**
* 希尔排序
* @param arrays 需要排序的序列
*/
public static void sort(int[] arrays){
if(arrays == null || arrays.length <= 1){
return;
}
//增量
int incrementNum = arrays.length/2;
while(incrementNum >=1){
for(int i=0;iarrays[j+incrementNum]){
int temple = arrays[j];
arrays[j] = arrays[j+incrementNum];
arrays[j+incrementNum] = temple;
}
}
}
//设置新的增量
incrementNum = incrementNum/2;
}
}
五. 快速排序(Quicksort)
1. 基本思想:(分治)
- 先从数列中取出一个数作为key值;
- 将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
- 对左右两个小数列重复第二步,直至各区间只有1个数。
1. 辅助理解:
- 初始时 i = 0; j = 9; key=72
由于已经将a[0]中的数保存到key中,可以理解成在数组a[0]上挖了个坑,可以将其它数据填充到这来。
从j开始向前找一个比key小的数。当j=8,符合条件,a[0] = a[8] ; i++ ; 将a[8]挖出再填到上一个坑a[0]中。
这样一个坑a[0]就被搞定了,但又形成了一个新坑a[8],这怎么办了?简单,再找数字来填a[8]这个坑。
这次从i开始向后找一个大于key的数,当i=3,符合条件,a[8] = a[3] ; j-- ; 将a[3]挖出再填到上一个坑中。
变化之前的数组:72 - 6 - 57 - 88 - 60 - 42 - 83 - 73 - 48 - 85
0 1 2 3 4 5 6 7 8 9
此时 i = 3; j = 7; key=72
再重复上面的步骤,先从后向前找,再从前向后找。
从j开始向前找,当j=5,符合条件,将a[5]挖出填到上一个坑中,a[3] = a[5]; i++;
从i开始向后找,当i=5时,由于i==j退出。
此时,i = j = 5,而a[5]刚好又是上次挖的坑,因此将key填入a[5]。
变化之前的数组:48 - 6 - 57 - 88 - 60 - 42 - 83 - 73 - 88 - 85
0 1 2 3 4 5 6 7 8 9
这里的i=3的位置,因为之前将值赋值给了i=8的位置,所以这里还没改变可以看出a[5]前面的数字都小于它,a[5]后面的数字都大于它。因此再对a[0…4]和a[6…9]这二个子区间重复上述步骤就可以了。
上一步得到的结果数组:48 - 6 - 57 - 42 - 60 - 72 - 83 - 73 - 88 - 85
0 1 2 3 4 5 6 7 8 9
3. 平均时间复杂度:O(N*logN)
快速排序中,基值key为序列的最大元素或者最小元素时,是最坏情况,时间复杂度为O(N^2)
4. 代码实现:
public static void quickSort(int a[],int l,int r){
if(l>=r)
return;
int i = l; int j = r; int key = a[l];//选择第一个数为key
while(i=key)//从右向左找第一个小于key的值
j--;
if(i
key值的选取可以有多种形式,例如中间数或者随机数,分别会对算法的复杂度产生不同的影响。
5.快速排序优化
具体可以查看快速排序优化https://blog.csdn.net/weizhengbo/article/details/77618963
//三数取中法:
//快速排序需要找基准值,为了避免找的基准值是最大的数,或者是最小的数,(基准值最大或者最小,效率最差)
//快速排序可用三数取中法优化,以下是三数取中法代码:
int ThreeToMid(int a, int b, int c)//1 3 5
{
if (a>b)
{
if (b > c)
return b;
else
{
if (a < c)
return a;
else
return c;
}
}
else
{
if (b < c)
return b;
else
{
if (a>c)
return a;
else
return c;
}
}
}
快速排序适用于数据杂乱无章的场景,而且越乱,快速排序的效率越体现的淋漓尽致。
六. 归并排序(Merge Sort)
1. 基本思想:
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。
首先考虑下如何将2个有序数列合并。这个非常简单,只要从比较2个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
//将有序数组a[]和b[]合并到c[]中
void MemeryArray(int a[], int n, int b[], int m, int c[])
{
int i, j, k;
i = j = k = 0;
while (i < n && j < m)
{
if (a[i] < b[j])
c[k++] = a[i++];
else
c[k++] = b[j++];
}
while (i < n)
c[k++] = a[i++];
while (j < m)
c[k++] = b[j++];
}
解决了上面的合并有序数列问题,再来看归并排序,其的基本思路就是将数组分成2组A,B,如果这2组组内的数据都是有序的,那么就可以很方便的将这2组数据进行排序。如何让这2组组内数据有序了?
可以将A,B组各自再分成2组。依次类推,当分出来的小组只有1个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的2个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。
2. 过程:
3. 平均时间复杂度:O(NlogN) 归并排序的效率是比较高的,设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。
4. 代码实现:
public static void merge_sort(int a[],int first,int last,int temp[]){
if(first < last){
int middle = (first + last)/2;// 找中间位置
merge_sort(a,first,middle,temp);//左半部分排好序
merge_sort(a,middle+1,last,temp);//右半部分排好序
mergeArray(a,first,middle,last,temp); //合并左右部分
}
}
//合并 :将两个序列a[first-middle],a[middle+1-end]合并
public static void mergeArray(int a[],int first,int middle,int end,int temp[]){
int i = first;
int m = middle;
int j = middle+1;
int n = end;
int k = 0;
while(i<=m && j<=n){
if(a[i] <= a[j]){
temp[k] = a[i];
k++;
i++;
}else{
temp[k] = a[j];
k++;
j++;
}
}
while(i<=m){
temp[k] = a[i];
k++;
i++;
}
while(j<=n){
temp[k] = a[j];
k++;
j++;
}
for(int ii=0;ii
按8位数字的序列举例:初始first=0,middle=3,end=7
那么第一个排序之后,分成两个序列调用:
(1)first=0,end=3,middle=1
first=0,end=1,middle=0
first=0,end=0
first=1,end=0
first=2,end=3,middle=2
first=2,end=2
first=3,end=3
(2)first=4,end=7,middle=5
first=4,end=5,middle=4
first=4,end=4
first=5,end=5
first=6,end=7,middle=6
first=6,end=6
first=7,end=7
最终的是排序当个数字的,然后将两个序列合并,依次从最终序列开始,依次将两个序列合并
七. 堆排序(HeapSort)
1. 基本思想:
2. ** 图示:** (88,85,83,73,72,60,57,48,42,6)
Heap Sort
3. 平均时间复杂度:O(NlogN)
由于每次重新恢复堆的时间复杂度为O(logN),共N - 1次重新恢复堆操作,再加上前面建立堆时N / 2次向下调整,每次调整时间复杂度也为O(logN)。二次操作时间相加还是O(N * logN)。
4. java代码实现:
//构建最小堆
public static void MakeMinHeap(int a[], int n){
for(int i=(n-1)/2 ; i>=0 ; i--){
MinHeapFixdown(a,i,n);
}
}
//从i节点开始调整,n为节点总数 从0开始计算 i节点的子节点为 2*i+1, 2*i+2
public static void MinHeapFixdown(int a[],int i,int n){
int j = 2*i+1; //子节点
int temp = 0;
while(j0;i--){
temp = a[0];
a[0] = a[i];
a[i] = temp;
MinHeapFixdown(a,0,i);
}
}
八. 基数排序(RadixSort)
BinSort
1. 基本思想:
BinSort想法非常简单,首先创建数组A[MaxValue];然后将每个数放到相应的位置上(例如17放在下标17的数组位置);最后遍历数组,即为排序后的结果。O(M*N)
2. ** 图示:**
3. ** 问题:** 当序列中存在较大值时,BinSort 的排序方法会浪费大量的空间开销。
RadixSort
1. 基本思想:
基数排序是在BinSort的基础上,通过基数的限制来减少空间的开销。
2. 过程:
过程1
过程2
(1)首先确定基数为10,数组的长度也就是10.每个数34都会在这10个数中寻找自己的位置。
(2)不同于BinSort会直接将数34放在数组的下标34处,基数排序是将34分开为3和4,第一轮排序根据最末位放在数组的下标4处,第二轮排序根据倒数第二位放在数组的下标3处,然后遍历数组即可。
3. java代码实现:
public static void RadixSort(int A[],int temp[],int n,int k,int r,int cnt[]){
//A:原数组
//temp:临时数组
//n:序列的数字个数
//k:最大的位数2
//r:基数10
//cnt:存储bin[i]的个数
for(int i=0 , rtok=1; i=0;j--){ //重点理解
cnt[(A[j]/rtok)%r]--;
temp[cnt[(A[j]/rtok)%r]] = A[j];
}
for(int j=0;j
九.排序总结
拿{ 6,2,4,6,1}举例。
有两个6,a[0]和a[3]。排序结果就有两种可能:
如果排序结束后,a[0]可以保证一定在a[3]前头,也就是他们原有的顺序不变,那这种排序算法就是稳定的。(比如常见的冒泡排序、基数排序、插入排序、归并排序、桶排序、二叉树排序等都是稳定的排序算法)
反之,如果不能保证原有顺序,这种算法就是不稳定的。(比如常见的选择排序,希尔排序,堆排序,快速排序等都是不稳定的排序算法)
十.稳定性分析:
冒泡排序:稳定
将n个记录看作按纵向排列,每趟排序时自下至上对每对相邻记录进行比较,若次序不符合要求(逆序)就交换。每趟排序结束时都能使排序范围内关键字最小的记录象一个气泡一样升到表上端的对应位置,整个排序过程共进行n-1趟,依次将关键字最小、次小、第三小…的各个记录“冒到”表的第一个、第二个、第三个… 位置上。
如:从小到大排序,那么算法就是从第一个开始,依次拿第一个、第二个往后做对比,前一个小于后一个,则不交换位置,大于后一个则交换位置,等于的也不交换位置。因此是稳定的
选择排序:不稳定
顾名思意,就是直接从待排序数组里选择一个最小(或最大)的数字,每次都拿一个最小数字出来,顺序放入新数组,直到全部拿完。再简单点,对着一群数组说,你们谁最小出列,站到最后边,然后继续对剩余的无序数组说,你们谁最小出列,站到最后边,再继续刚才的操作,一直到最后一个,继续站到最后边,现在数组有序了,从小到大。
选择排序的做法是从第一个位置开始,依次拿第一个、第二个、第三个到第N个去与后面做对比,找到后面最小的一个,做位置交换。那么这样的做法,就会可能造成相同的数字位置前后出现交换的情况。
例如:5 8 5 2 9这个数列,第一次拿第一个5去与后面的做比较,后面最小的是2,那么5跟2做比较,交换位置。
插入排序:稳定
插入排序就是每一步都将一个待排数据按其大小插入到已经排序的数据中的适当位置,直到全部插入完毕。插入排序方法分直接插入排序和折半插入排序两种。折半插入排序基本思想和直接插入排序一样,区别在于寻找插入位置的方法不同,折半插入排序采用折半查找法来寻找插入位置。折半查找法只能对有序的序列使用。基本思想就是查找插入位置的时候,把序列分成两半(选择一个中间数mid),如果带插入数据大于mid则到右半部分序列去在进行折半查找;反之,则到左半部分序列去折半查找。
第n次排序,待排序是第n+1个数字,那么就拿这个数字向前去做比较,只要是小于前面一个数字,就往前面交换,直到前面一个数字小于这个数字为止。那么如果两个数字是一样的,就不会发生位置交换的情况,那么就是稳定的。
其实插入排序,就是在第n+1个数字去与前面的数字做比较,从后往前比较,根据需要将更小的或者更大的往前移动。因为是从第n+1个数字开始去比较排序插入的,因此第一个数字可以忽略,所以是排length-1次。
快速排序:不稳定
通过一趟扫描将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
快速排序的做法,就是每一次都将序列的第一个数字看成key,然后下标为0的看成是i,序列的最后一个的下标看成是j,然后从后向前找出比key小的数字,这个时候j也变成这个数字的下标,将这个数字填入i的位置上,然后再从前向后找到比key大的数字,填入最新的j的位置上,这个时候i的位置也变成了这个比key大的数字的下标,交换完成之后,继续从后向前找小的数字,从前向后找大的数字,依次将小的数字填入i的位置,大的数字填入最新的j的位置上,直到i==j的时候,将key填入i的位置上。然后将key的前面的数字和后面的数字分别看成是一个序列,把这两个序列重复上面的步骤。
那么在交换数字的过程中就可能出现相同数字顺序发生变化的情况。例如:
24,58,58,22,98这个序列,i=0,j=4,key=24,那么第一次就是22移动到i=0的位置上,第一个58比24大,移动到j=3的位置上,那么第一个58和第二个58的位置就出现了变化,所以不稳定。
public static void quickSort(int a[],int l,int r){
if(l>=r)
return;
int i = l; int j = r; int key = a[l];//选择第一个数为key
while(i=key)//从右向左找第一个小于key的值
j--;
if(i
归并排序:稳定
把原始数组分成若干子数组,对每一个子数组进行排序,继续把子数组与子数组合并,合并后仍然有序,直到全部合并完,形成有序的数组。
归并序列,是依次将序列二等分,直到分成每个序列只有一个数字为止,然后依次对每个序列做排序,然后再两两合并两个序列。这样的排序合并,当出现数字相同的时候,并不会交换顺序。
public static void merge_sort(int a[],int first,int last,int temp[]){
if(first < last){
int middle = (first + last)/2;// 找中间位置
merge_sort(a,first,middle,temp);//左半部分排好序
merge_sort(a,middle+1,last,temp);//右半部分排好序
mergeArray(a,first,middle,last,temp); //合并左右部分
}
}
//合并 :将两个序列a[first-middle],a[middle+1-end]合并
public static void mergeArray(int a[],int first,int middle,int end,int temp[]){
int i = first;
int m = middle;
int j = middle+1;
int n = end;
int k = 0;
while(i<=m && j<=n){
if(a[i] <= a[j]){
temp[k] = a[i];
k++;
i++;
}else{
temp[k] = a[j];
k++;
j++;
}
}
while(i<=m){
temp[k] = a[i];
k++;
i++;
}
while(j<=n){
temp[k] = a[j];
k++;
j++;
}
for(int ii=0;ii
基数排序:稳定
一个元素有多个关键字,定义排序后的“有序”是指依次比较这些关键字,不同的直接按其大小关系,相同的比较后续的关键字,例如字符串与数字。然后,这些关键字都有一些范围。依次选取这些关键字作为依据,进行依次分类,这样,类别之间就有了相对的大小关系。然后,对每个类别进行相同的操作,直至所有关键字都被比较过为止。
基础排序,就是根据0-9这十个基数,依次针对个位、十位、百位等做重新排序。
比如个位的,按顺序进行,如果个位为0的,则放在0位上,为1的,放在1位上,依次进行,然后从零开始,将所有的数字重新组合起来,按之前的顺序。十位上的也是这样进行,如果没有百位,则就可以排列好。这样的情况,如果是两个相同的数字,那么在排序的过程中,顺序永远不会发生改变,因此是稳定的。
public static void RadixSort(int A[],int temp[],int n,int k,int r,int cnt[]){
//A:原数组
//temp:临时数组
//n:序列的数字个数
//k:最大的位数2
//r:基数10
//cnt:存储bin[i]的个数
for(int i=0 , rtok=1; i=0;j--){ //重点理解
cnt[(A[j]/rtok)%r]--;
temp[cnt[(A[j]/rtok)%r]] = A[j];
}
for(int j=0;j
希尔排序:不稳定
希尔排序的实质就是分组插入排序,该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。
该方法的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。
希尔排序,就是将整个序列分析若干个子序列,比如循环除以2,直到得到余数为1的时候,这样的话,就可以依次取到序列的间隔,就是每个子序列都只有两个数字,然后交换位置,然后再扩大子序列的长度,而每次子序列的排序都是一次插入排序,一次插入排序是稳定的,但是多个不同的序列同时进行插入排序的时候,就会有可能造成相同的元素的位置顺序改变,所以是不稳定的。
public static void shell_sort(int array[],int lenth){
int temp = 0;
int incre = lenth;
while(true){
incre = incre/2;
for(int k = 0;kk;j-=incre){
if(array[j]
堆排序:不稳定
堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。
既然是堆排序,自然需要先建立一个堆,而建堆的核心内容是调整堆,使二叉树满足堆的定义(每个节点的值都不大于其父节点的值)。调堆的过程应该从最后一个非叶子节点开始
例子
72,6,57,88,60,42,83,73,48,85
这个序列,先创建一个二叉树,依次将序列上的数字放置在二叉树节点上,当这个过程中,如果有子节点大于父节点的,则交换二者的位置,每放置一个,就得判断依次子节点是否大于父节点,如果是大于的,就立马交换位置。直到父节点都是大于子节点为止。
(1)然后将根节点和最后一个节点的位置交换,再将最后一个节点放置在数组的最后一位,然后重新调整二叉树,直到二叉树的根节点是剩下的数字中最大的为止。
(2)继续重复第一步的步骤,将根节点数字与最后一个节点的数字交换位置,然后将最后一个数字放置在数组的倒数第二个位置上,依次反复,直到没有节点为止。
本文内存来自其他博客转载,本文纯属自己记忆