目录
一、排序算法简介
排序就是使数据按照某种规则有序排列,一般分为降序和升序。
排序有非常多的算法,在对这些算法进行检验时我们需要考虑以下几个因素,
- 时间复杂度:从序列的初始状态到经过排序算法的变换移位等操作变到最终排序好的结果状态的过程所花费的时间度量
- 空间复杂度:从序列的初始状态经过排序移位变换的过程一直到最终的状态所花费的空间开销
- 稳定性:在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,则称算法稳定,否则不稳定
- 比如经过算法降序排列后得到,则称该算法稳定;若得到,则称算法不稳定
常见的排序算法如下:
算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
直接插入排序 | √ | ||
折半插入排序 | √ | ||
直接选择排序 | √ | ||
冒泡排序 | √ | ||
希尔排序 | × | ||
快速排序 | × | ||
堆排序 | × | ||
归并排序 | √ |
接下来我们一一介绍(后续会持续更新)
二、直接插入排序
思路:
直接插入排序的思路是,当插入第i个元素时,前面的i-1个元素是已经排好序的,
这时只需要找到插入好的位置就可以将第i个元素插入,此时数组是有序排列的。
实现:
//直接插入排序
public static int[] InsertSort(int[] arr){
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < i; j++) {
if(arr[i]<arr[j]){
swap(arr,i,j);
}
}
}
return arr;
}
结果:
直接插入排序的时间复杂度是,空间复杂度是,是稳定的算法
三、折半插入排序
思路:
折半插入排序和直接插入排序的思想差不多,但是折半插入排序在寻找第i个元素插入的位置时使用了折半查找,
能够使用折半查找的前提是数组有序,恰好这也是插入排序的一个特点,前i-1个元素已经处于有序状态了。
实现:
//折半插入排序
public static int[] BinaryInsertSort(int[] arr){
for (int i = 1; i < arr.length; i++) {
int cur=arr[i];
int low=0;
int high=i-1;
while(low<=high){//查找要插入的位置,并将下标放到low中
int mid=(low+high)/2;
if(arr[mid]>cur) {
high=mid-1;
}else {
low=mid+1;
}
}
for (int j = i; j > low ; j--) {
arr[j]=arr[j-1];
}
arr[low]=cur;
}
return arr;
}
结果:
折半插入排序的时间复杂度是,空间复杂度是,是稳定的算法
四、直接选择排序
思路:
直接选择排序的思路更加简单,就是第一轮选出一个最小(大)的数放在第一个位置,
下一轮就不考虑之前选过的元素,在剩下的元素中选出最大的放在第二个位置,依次类推,直到剩下的元素个数为1,此时数组必定有序。
实现:
//直接选择排序
public static int[] SelectSort(int[] arr){
for (int i = 0; i < arr.length; i++) {
for (int j = i; j < arr.length; j++) {
if(arr[i]>arr[j]){
swap(arr,i,j);
}
}
}
return arr;
}
结果:
折半插入排序的时间复杂度是,空间复杂度是,是稳定的算法
五、冒泡排序
思路:
冒泡排序的思路类似于水中吐泡泡,轻的元素会自动浮上水面,重的元素会沉入水底。
我们首先从第一个元素开始比较,比较它和后面一个元素的大小,如果前者数值更大,那么就交换两个元素的位置,让重的元素慢慢下沉,
然后比较第二个和第三个,如果前面的元素大就交换位置,重的元素下沉,一直到比较最后两个元素的大小,重的下沉,这样一轮下来就可以保证最大的元素沉到了数组的末尾,
那么下一次循环的时候只用对前n-1个元素进行冒泡即可,每一轮可以确定一个元素的最终位置,进行n-1轮数组就会变为有序状态了。
实现:
//冒泡排序
public static int[] BubbleSort(int[] arr){
for (int i = 0; i < arr.length-1; i++) {
for (int j = 0; j < arr.length-i-1; j++) {
if(arr[j]>arr[j+1]){
swap(arr,j,j+1);
}
}
}
return arr;
}
结果:
冒泡排序的时间复杂度是,空间复杂度是,是稳定的算法
六、希尔排序
思路:
希尔排序是插入排序的一种改进版本,其基本思路是将数组分为若干个小的子数组分别进行插入排序,
其分组的策略是取整数gap,让下标相隔gap的元素分为一组,对该组的元素进行插入排序,
然后下次减少gap的值,直到gap取1时,使用完插入排序后整个数组就变得有序了。
实现:
//希尔排序
public static int[] ShellSort(int[] arr){
int gap=arr.length;
do{
gap=gap/3+1;//希尔初始设置的减量为gap=gap/2;
System.out.println("----------------");
System.out.println(gap);
for (int i = 0; i < gap; i++) {//对每个子数组进行插入排序
System.out.println("------");
for (int j = i+gap; j < arr.length; j+=gap) {
System.out.println("j:"+j);
int k=j-gap;
while(k>=0&&arr[k]>arr[k+gap]){
swap(arr,k,k+gap);
k-=gap;
}
print(arr);
}
}
}while(gap>1);
return arr;
}
结果:
直接插入排序的时间复杂度是,空间复杂度是,是不稳定的算法
七、快速排序
思路:
快速排序的思路就是首先选取数组的第一个元素作为基准,将数组分为两部分,
左边存放的都是小于基准的元素,右边存放的是大于基准的元素,这样基准元素所在位置就是它最终的位置,接着递归调用左右两边数组进行排序。
实现:
//快速排序
public static int[] QuickSort(int[] arr,int low,int high){
if(low<high){
int curpos=Partition(arr,low,high);
QuickSort(arr,low,curpos-1);
QuickSort(arr,curpos+1,high);
}
return arr;
}
public static int Partition(int[] arr,int low,int high){
int curPos=low;
int curVal=arr[low];
for (int i = low+1; i <= high; i++) {
if(arr[i]<curVal){
curPos++;//循环结束之后可以找到当前元素在数组中的下标
if(curPos!=i){//将处于右边小于当前元素的数交换到左边
swap(arr,curPos,i);
}
}
print(arr);
}
arr[low]=arr[curPos];//将当前元素放置到应位置
arr[curPos]=curVal;
return curPos;
}
结果:
直接插入排序的时间复杂度是,空间复杂度是,是不稳定的算法
八、堆排序
思路:
堆排序就是利用堆的特性,即父节点的值永远比孩子节点的值大,那么根节点存储的就是整个数组中最大的值。那么如何构建这样一个堆结构呢?
首先我们按照数组的下标建立一个二叉树结构,然后从最大的非叶子节点开始处理,将其与子结点中较大的那个进行比较,如果子结点有比其值更大,那么就交换两个节点的值,将大的元素往上浮,小的元素往下沉,
因为这个时候改变了树的结构,小的元素往下沉了,不能保证下沉的元素的子结点都小于该节点,所以要对子结点也同时进行调整,
直到下沉到某个节点,其子结点都是小于该元素的。
对所有的非叶子节点进行调整直到根节点,这样整个二叉树就被调整成为堆了,根节点就是最大元素。
接下来就是对数组进行排序,我们首先调换数组第一个位置元素与数组最后一个位置元素,这样最大的元素就放到了数组末尾,其位置也确定了。
然后对前n-1个元素进行调整重新建立新的最大堆,选取出第二大的元素在根节点,然后调换倒数第二的位置,倒数第二大的元素位置也确定了,
以此类推,最后就可以得到一个有序的数组。
实现:
//堆排序
public static int[] heapSort(int[] arr) {
for (int i = arr.length/2-1; i >= 0; i--) {
adjustHeap(arr,i,arr.length);
}
for (int j = arr.length-1; j >0 ; j--) {
swap(arr,0,j);
adjustHeap(arr,0,j);
}
return arr;
}
//调整堆:arr 待组堆, i 起始结点, length 堆的长度
public static void adjustHeap(int[] arr, int i, int length) {
int temp=arr[i];
for (int k = 2*i+1; k < length; k=2*k+1) {
if(k+1<length && arr[k]<arr[k+1]){
k++;
}
if(arr[k]>temp){
swap(arr,i,k);
i=k;//对调换之后的节点进行调整
}else{
break;
}
}
}
结果:
直接插入排序的时间复杂度是,空间复杂度是,是不稳定的算法
九、归并排序
思路:
归并排序的思路是先将数组不断的二分,得到两个数组,一直到数组中只有一个元素,
然后反过来进行归并,归并的时候定义两个指针,同时指向两个数组的初始位置,比较指针所指元素的大小,
值小的元素放入结果数组中对应位置,然后指针后移,直到两个数组的元素都被放入到结果数组的对应位置中。
实现:
//归并排序
public static int[] MergeSort(int[] arr){
int[] res=new int[arr.length];
mergeSort(arr,res,0,arr.length-1);
return arr;
}
public static void mergeSort(int[] arr,int[] res,int low,int high){
if(low>=high){
return;
}else{
int mid=(low+high)/2;
mergeSort(arr,res,low,mid);
mergeSort(arr,res,mid+1,high);
merge(arr,res,low,mid,high);
}
}
public static void merge(int[] arr,int[] res,int low,int mid,int high){
for (int i = low; i <= high; i++) {
res[i]=arr[i];
}
int i=low;
int j=mid+1;
int k=low;
while(i<=mid && j<=high) {
if(res[i]>res[j]){
arr[k++]=res[j++];
}else{
arr[k++]=res[i++];
}
}
while(i<=mid){
arr[k++]=res[i++];
}
while(j<=high){
arr[k++]=res[j++];
}
}
结果:
直接插入排序的时间复杂度是,空间复杂度是,是稳定的算法