1.排序算法概述
十种常见的排序算法可以分为两大类:
**比较类排序:**通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也被称为非线性时间比较类排序。
**非比较类排序:**不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称之为线性非比较类排序。
2.交换排序
2.1 冒泡排序(Bubble Sort)
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
public class bubbleTest{
public static void bubbleSort(int[] arr){
int n=arr.length;
for(int i=0;i<n-1;i++){
for(int j=0;j<n-1-i;j++){
if(arr[j]>arr[j+1]){
int temp=arr[j+1];
arr[j+1]=arr[j];
arr[j]=temp;
}
}
}
}
public static void main(String[] args){
int[] a={29,1,45,23,15};
bubbleSort(a);
System.out.println("从小到大排序后的结果是:");
for(int i=0;i<a.length;i++)
System.out.print(a[i]+" ");
}
}
输出为
从小到大排序后的结果是:
1 15 23 29 45
注意:一次冒泡排序会让至少一个元素移动到最初位置(即最大元素),重复了n次,就完成了n个数据的而排序,当某次冒泡排序已经没有数据交换时,说明已经达到了完全有序。
冒泡排序的时间复杂度为O(n^2),空间复杂度为O(1),是一个稳定的排序算法。
2.2 快速排序(Bubble Sort)
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
1.从数列中挑出一个元素,称为 “基准”(pivot);
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
其更生动的过程可以参考java 快速排序的配图。
public class quickTest{
public static void quickSort(int[] a,int low,int height){
int i=low;
int j=height;
if(i>j){
return;
}
int k=a[i];//k为基准点的值
while(i<j){
while(i<j&&a[j]>k){
j--;
}
while(i<j&&a[i]<=k){//注意这个等于,非常重要!
i++;
}
if(i<j){
int temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
//交换
k=a[i];
a[i]=a[low];
a[low]=k;
//对左边进行排序,递归算法
quickSort(a,low,i-1);
//对右边进行排序,递归算法
quickSort(a,i+1,height);
}
public static void main(String[] args){
int[] a={29,1,45,23,15};
quickSort(a,0,a.length-1);
System.out.println("从小到大排序后的结果是:");
for(int i=0;i<a.length;i++)
System.out.print(a[i]+" ");
}
}
快速排序的时间复杂度为O(nlogn),空间复杂度为O(1),是一个不稳定的排序算法。
3.插入排序
3.1 简单插入排序
一般来说,插入排序都在数组上实现。具体算法描述如下:
从第一个元素开始,该元素可以认为已经被排序;
取出下一个元素,在已经排序的元素序列中从后向前扫描;
如果该元素(已排序)大于新元素,将该元素移到下一位置;
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
将新元素插入到该位置后;
重复步骤2~5。
public class insertTest {
public static void insertSort(int[] arr){
for(int i=1;i<arr.length;i++){
for(int j=i;j>0;j--){
if(arr[j-1]>arr[j]){
int temp=arr[j-1];
arr[j-1]=arr[j];
arr[j]=temp;
}
}
}
}
public static void main(String[] args){
int[] a={29,1,45,23,15};
insertSort(a);
System.out.println("从小到大排序后的结果是:");
for(int i=0;i<a.length;i++)
System.out.print(a[i]+" ");
}
}
简单插入排序的时间复杂度为O(n^2),空间复杂度为O(1),是一个稳定的排序算法。
3.2 希尔排序
1959年Shell发明,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
按增量序列个数k,对序列进行k 趟排序;
每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
public class shellTest {
public static void shellSort(int[] arr){
for(int step=arr.length/2;step>0;step/=2){
for(int i=step;i<arr.length;i++){
int j=i;
while(j-step>=0&&arr[j]<arr[j-step]){
swap(arr,j,j-step);
j-=step;
}
}
}
}
public static void swap(int []arr,int a,int b){
arr[a]=arr[a]+arr[b];
arr[b]=arr[a]-arr[b];
arr[a]=arr[a]-arr[b];
}
public static void main(String[] args){
int[] a={29,1,45,23,15};
shellSort(a);
System.out.println("从小到大排序后的结果是:");
for(int i=0;i<a.length;i++)
System.out.print(a[i]+" ");
}
}
4.选择排序
4.1 简单选择排序
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
public class selectTest {
public static void selectSort(int[] arr){
int temp;
for(int i=0;i<arr.length;i++){
int min=i;
for(int j=i+1;j<arr.length;j++){
if(arr[j]<=arr[min]){
min=j;
}
}
temp=arr[min];
arr[min]=arr[i];
arr[i]=temp;
}
}
public static void main(String[] args){
int[] a={29,1,45,23,15};
selectSort(a);
System.out.println("从小到大排序后的结果是:");
for(int i=0;i<a.length;i++)
System.out.print(a[i]+" ");
}
}
4.2 堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
首先,我们看一下什么是完全二叉树和满二叉树:
完全二叉树:除了最后一层之外的其他每一层都被完全填充,并且所有的节点都保持向左对齐。
满二叉树:除了叶子结点之外的每一个结点都有两个孩子,每一层(当然包含最后一层)都被完全填充。
简单来说:堆排序就是将数据看成是完全二叉树、根据完全二叉树的特性来进行排序的一种算法。
算法详细的过程见堆排序
public class heapTest{
public static void swap(int[] arr,int i,int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
public static void heapSort(int[] arr){
//构建大根堆
int len=arr.length;
for(int i=len/2-1;i>=0;i--){
//从第一个非叶子节点从下至上,从右至左调整
adjustHeap(arr,i,len);
}
for(int j=len-1;j>0;j--){
swap(arr,0,j);
adjustHeap(arr,0,len--);
}
}
public static void adjustHeap(int[] arr,int i,int j){
int temp=arr[i];
for(int k=i*2+1;k<arr.length;k=k*2+1){//k=k*2+1向下移
if(arr[k+1]>arr[k]){//左右节点比较
k+=1;
}
if(arr[k]>temp){//由于temp存储了arr[i]的值所以直接赋值就可以
arr[i]=arr[k];
i=k;
}else{
break;
}
}
arr[i]=temp;
}
public static void main(String[] args){
int[] a={29,1,45,23,15};
heapSort(a);
System.out.println("从小到大排序后的结果是:");
for(int i=0;i<a.length;i++)
System.out.print(a[i]+" ");
}
}
5.归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
1.把长度为n的输入序列分成两个长度为n/2的子序列;
2.对这两个子序列分别采用归并排序;
3.将两个排序好的子序列合并成一个最终的排序序列。
public class mergeTest {
public static void mergeSort(int[] arr,int n){
mergeSortRecursion(arr,0,n-1);
}
public static void mergeSortRecursion(int[] arr,int p,int r){
//递归终止条件
if(p>=r){
return;
}
//取p到r之间的位置q
int q=p+(r-p)/2;
//分治递归
mergeSortRecursion(arr,p,q);
mergeSortRecursion(arr,q+1,r);
//合并数组
merge(arr,p,q,r);
}
//合并两个有序数组
public static void merge(int[] arr,int p,int q,int r){
int i=p;
int j=q+1;
int k=0;
int[] temp=new int[r-p+1];
//最少把一个数组中的数据取完
while(i<=q&&j<=r){
if(arr[i]<=arr[j]){
temp[k++]=arr[i++];
}
else{
temp[k++]=arr[j++];
}
}
//判断哪个子数组中有剩余的数据,先假设前面的子数组还有剩余数据
int start=i;
int end=q;
if(j<=r){//即后面的子数组还有剩余数据
start=j;
end=r;
}
while(start<=end){
temp[k++]=arr[start++];
}
System.arraycopy(temp, 0, arr, p, r-p+1);
}
public static void main(String[] args){
int[] a={29,1,45,23,15};
mergeSort(a,5);
System.out.println("从小到大排序后的结果是:");
for(int i=0;i<a.length;i++)
System.out.print(a[i]+" ");
}
}
6.非比较排序
6.1 计数排序
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
找出待排序的数组中最大和最小的元素;
统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
public class countingTest {
public static int[] countingSort(int[] arr,int k){//特别注意以下这个k,这个k表示待排序数组中的最大值,比如这个例子中,数组为{2,5,3,0,2,3,0,3}即待排序数组中元素的范围为0-5,则k的取值应该为5.
int[] C=new int[k+1];
int length=arr.length,sum=0;
int[] B=new int[length];
for(int i=0;i<length;i++){
C[arr[i]]+=1;
}
for(int i=0;i<k+1;i++){
sum+=C[i];
C[i]=sum;
}
for(int i=length-1;i>=0;i--){
B[C[arr[i]]-1]=arr[i];
C[arr[i]]--;
}
return B;
}
public static void main(String[] args){
int[] A={2,5,3,0,2,3,0,3};
int[] B=countingSort(A,5);
System.out.println("从小到大排序后的结果是:");
for(int i=0;i<A.length;i++){
System.out.println((i+1)+"th:"+B[i]);
}
}
}
6.2 桶排序
桶排序的基本思想是: 把数组 arr 划分为n个大小相同子区间(桶),每个子区间各自排序,最后合并。
计数排序是桶排序的一种特殊情况,可以把计数排序当成每个桶里只有一个元素的情况。
1.找出待排序数组中的最大值max、最小值min
2.我们使用 动态数组ArrayList 作为桶,桶里放的元素也用 ArrayList 存储。桶的数量为(max-min)/arr.length+1
3.遍历数组 arr,计算每个元素 arr[i] 放的桶
4.每个桶各自排序
5.遍历桶数组,把排序好的元素放进输出数组
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class bucketTest {
public static void bucketSort(int[] arr){
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i = 0; i < arr.length; i++){
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
//桶数
int bucketNum = (max - min) / arr.length + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for(int i = 0; i < bucketNum; i++){
bucketArr.add(new ArrayList<Integer>());
}
//将每个元素放入桶
for(int i = 0; i < arr.length; i++){
int num = (arr[i] - min) / (arr.length);
bucketArr.get(num).add(arr[i]);
}
//对每个桶进行排序
for(int i = 0; i < bucketArr.size(); i++){
Collections.sort(bucketArr.get(i));
}
System.out.println(bucketArr.toString());
}
public static void main(String[] args){
int[] a={29,1,45,23,15};
bucketSort(a);
}
}
6.3 基数排序
先按数组每个数的个位数进行排序 再将十位数排序 再将百位数排序 以此类推 没有十位数百位数…的补0。当所有位上的数排完序后 整个数组就是有序的。底下有java代码实现。
-----以下是具体实现步骤
1.先获取数组中的最大数 目的是需要知道一共有几位数 排几次序。
2.定义一个临时数组 int[] temp = new int[array.length]每次排好序就将他们放进这个临时数组。
4.定义一个桶数组 桶的索引是0到9 遍历数组将每个数(对应位的数字)放进与之对应的桶里 并且计算桶里数的个数。
5.再次遍历这个桶数组,将桶里数的个数变为 前面桶的个数加上自己桶里的个数 (bucket[i] += bucket[i-1]) 这一步完成之后 我们就可以从桶里知道对应数排序后的位置
索引就是其位置-1。
6.既然可以知道每位数排序后的索引了 那我们就来开始排序: 从后遍历该数组 特别注意是从后遍历数组 根据该数对应位上的数字找到对应的桶从而得到桶里的数字i 该数排序后的索引位置就为i-1 之后将其放入临时数组中 temp[i-1];
别忘了该桶里的数字还要-1 因为 下一个相同的数来找索引 需要放在上一个数的前面;
7.最后一步我们需要把临时数组中的数赋值给原数组 然后接着下一位数的排序
public class radixTest {
public static void radixSort(int[] arr){
int max=getMax(arr);
int bit=1;//首先比较个位数
while(max/bit>0){
radix(arr,bit);
bit*=10;//然后向后比较位数
}
}
private static void radix(int[] arr,int bit){
int[] temp=new int[arr.length];
int[] bucket=new int[10];
for(int i=0;i<arr.length;i++){
bucket[(arr[i]/bit)%10]++;
}
for(int i=1;i<bucket.length;i++){
bucket[i]+=bucket[i-1];
}
for(int i=arr.length-1;i>=0;i--){
temp[bucket[(arr[i]/bit)%10]-1]=arr[i];
bucket[(arr[i]/bit)%10]--;
}
for(int i=0;i<temp.length;i++){
arr[i]=temp[i];
}
}
private static int getMax(int[] array) {
int max = array[0];
for(int i = 1; i < array.length; i++){
if(array[i] > max) {
max = array[i];
}
}
return max;
}
public static void main(String[] args) {
int[] array = {0,2,11,3,1,5,9,8,7};
new radixTest().radixSort(array);
for (int i : array) {
System.out.println(i);
}
}
}