面试常考的排序算法

目录

 

预备知识

冒泡排序

算法

 冒泡排序的分析

冒泡排序的优化

选择排序 

算法

选择排序分析 

堆排序

算法

堆排序的分析

直接插入排序

算法

插入排序的分析 

希尔排序

算法

希尔排序的分析

快速排序

算法

快速排序的分析

快速排序的优化

快速排序非递归


预备知识

被排序的对象属于Comparable类型,因此可以使用ComparaTo方法对输入数据施加相容的排序,除(引用)赋值运算外,这是仅有的对输入数据进行操作。在这些条件下的排序叫做基于比较的排序。

排序的稳定性:如果 2  9  3  5  1  3'  10 ,排序完是1  2  3  3'  5  9  10则就是稳定的排序,排序完是1  2  3'  3  5  9  10则就是不稳定的排序。判定方法是稳定的排序没有发生跳跃性的交换。稳定的排序可以变为不稳定的排序,不稳定的排序不能变为稳定的排序。

关于内部排序和外部排序,内部排序是在内存上进行排序,针对于元素个数相对来说比较小(小于几百万)。外部排序是在磁盘上进行排序,针对于元素个数较大的排序。本篇文章前七种属于内部排序,最后一种是外部排序。

冒泡排序

算法

使用两个for循环,第一个for循环是表示趟数i,第二个for循环是表示 比较次数j。相邻元素进行比较,满足条件进行交换。比较次数p之后,会有p个数据有序。

原始数组863947比较次数
第一趟之后6384795
第二趟之后3647894
第三趟之后3467893
第四趟之后3467892
第五趟之后3467891
    public static void bubbleSort(int[] array) {
        for (int i = 0; i < array.length-1 ; i++) {
            for (int j = 0; j < array.length-1-i; j++) {
                if (array[j] > array[j+1]) {
                    int tmp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = tmp;
                }
            }
        }
    }

 冒泡排序的分析

两层for循环,时间复杂度O(N)。
没有申请额外空间,空间复杂度:O(1)
稳定性:稳定的

冒泡排序的优化

我们会发现上面的例子在第三趟的时候已经有序了,因此不用再进行比较了。那么代码怎么知道有序了,是在比较的过程中如果没有进行交换了说明就有序了。我们可以定义一个boolean类型的变量进行如下优化。

public static void bubbleSort(int[] array) {
    boolean flg = false;
    for (int i = 0; i < array.length-1; i++) {
        for (int j = 0; j < array.length-1-i; j++) {
            if (array[j] > array[j+1]) {
                int tmp = array[j];
                array[j] = array[j+1];
                array[j+1] = tmp;
                flg = true;
            }
        }
        if (!flg) {
            break;
        }
    }
}

选择排序 

算法

i从0号下标开始,选择后面比它小的元素和它交换。一趟排序完成第一个元素就有序了。

    public static void selectSort(int[] array) {
        for (int i = 0; i < array.length; i++) {
            for (int j = i+1; j < array.length; j++) {
                if (array[i] > array[j]) {
                    int tmp = array[i];
                    array[i] = array[j];
                    array[j] = tmp;
                }
            }
        }
    }

选择排序分析 

时间复杂度:O(n^2)[最好和最坏都是]
空间复杂度:O(1)
稳定性:不稳定

堆排序

算法

把数组通过层序遍历的方式建立一个堆。如果要进行升序排序,要调整为一个大根堆。那么最顶层的元素就是有序的,就是最大的元素。如果进行降序排序,要调整为一个小根堆,那么最顶层的元素就是有序的,就是最小的元素。

具体做法是:
1.要调整为一个大根堆(从小到大排序)。那么就是保证每一颗子树都是大根堆,从上往下调整,即就是向下调整。调整的方式是,让父亲节点和左右孩子的最大值比较(前提是有右孩子),要保证有右孩子,就是判断child+1是否小于length。如果孩子节点大于父亲节点,两者交换即可。【这只是调整了一次】 第二次调整只需要让根节点-1继续进行调整。
2.进行排序,第一个元素和最后一个元素进行交换,最后一个元素就有序了。调整0号下标的那棵树为大根堆。定义一个end,每交换一次end--,进行向下调整。当end=0说明调整完成。

 //向下调整
    public static void adjustDown(int[] array, int root, int len) {
        int parent = root;
        int child = 2*parent+1;
        while (child < len) {
            if (child+1 < len && array[child] < array[child+1]) {
                child++;
            }
            //此时child就是指向子孩子的较大值
            if (array[child] > array[parent]) {
                int tmp = array[child];
                array[child] = array[parent];
                array[parent] = tmp;
                //调整子树也要是大根堆
                parent = child;
                child = 2*parent+1;
            } else {
                break;
            }
        }
    }

    //每棵树都向下调整
    public static void creatHeap(int[] array) {
        for (int i = (array.length-1-1)/2; i >= 0; i--) {
            adjustDown(array, i, array.length);
        }
    }

    //进行排序
    public static void heapSort(int[] array) {
        creatHeap(array);
        int end = array.length-1;
        while (end > 0) {
            int tmp = array[0];
            array[0] = array[end];
            array[end] = tmp;
            //adjustDown取不到len 所以先调整后end--
            adjustDown(array, 0, end);
            end--;
        }
    }

    public static void main(String[] args) {
        int[] array = new int[]{27, 15, 19, 18, 28, 34, 65, 49, 25, 37};
        heapSort(array);
        System.out.println(Arrays.toString(array));
    }

堆排序的分析

时间复杂度:由于根节点和叶子节点个数大概是1:1,选根节点是N/2,每个根节点都要进行向下调整log2N。再加上排序的O(N),即就是log2N(2为底)*N/2 + O(N)== Nlog2N

空间复杂度:O(1)

最好、最坏、平均复杂度都是Nlog2N

建堆的时间复杂度:Nlog2N

一次调整的时间复杂度:log2N

稳定性:不稳定

 

直接插入排序

算法

排序有N-1趟排序组成,对于p=1到p=N-1,插入排序保证0-p位置上的元素为已排序状态。排序情况如下:

原始数组34864513221
p=1趟之后83464513221
p=2趟之后83464513221
p=3趟之后83451643221
p=4趟之后83234516421
p=5趟之后2132345164

这个排序的做法我们可以类比于揭扑克牌。第一个数据已经有序,i应该从1开始。把i号下标的数据放到tmp。比较i号下标前面的元素,下标为j=i-1。j号下标的元素和tmp相比较。如果j号下标元素比tmp大,把j号下标元素放到j+1号下标,j--,当j比0小时,说明前面没有元素了。再把tmp里的数据放到j+1位置。这是一趟快速排序。要注意,如果tmp比j号元素大,说明前面已经有序,直接把tmp放到j+1。

    public static void insertSort(int[] array) {
        int j = 0;
        for (int i = 1; i < array.length; i++) {
            int tmp = array[i];
            for (j = i-1; j >= 0; j--) {
                if (tmp < array[j]) {
                    array[j+1] = array[j];
                } else {
                    break;
                }
            }
            array[j+1] = tmp;
        }
    }

插入排序的分析 

有两层for循环,如果输入数据有序,内层循环的检验总是立即判定不成立而终止,所以最好时间复杂度是O(N),最坏的是O(N^2),即就是无序的时候。并且数据越有序越快。

没有申请额外空间,因此空间复杂度是O(1)

该排序是稳定的,若把条件if (tmp < array[j])改为if (tmp <= array[j])该排序就变为不稳定的排序了。

希尔排序

算法

希尔排序直接插入排序的优化。分组进行排序,越有序直接插入排序越快。增量一定互为素数,最后一个增量必须为1。分组我们可以采用跳跃式分组,跳跃式的分组会让比较大的数据在后面,比较小的数据在前面(升序的情况),使得数据更佳具有有序性。

    public static void shell(int[] array, int gap) {
        for (int i = gap; i < array.length; i++) {
            int tmp = array[i];
            int j = 0;
            for (j = i-gap; j >= 0; j -= gap) {
                if (array[j] > tmp) {
                    array[j+gap] = array[j];
                } else {
                    break;
                }
            }
            array[j+gap] = tmp;
        }
    }

    public static void shellSort(int[] array) {
        //分的组数
        int[] drr = {5,3,1};
        for (int i = 0; i < drr.length; i++) {
            shell(array, drr[i]);
        }
    }

 

做法,分别进行了5、3、1的分组,随着分组的改变,数据越来越有序。 

希尔排序的分析

时间复杂度:O(n^1.3 - n^1.5)

空间复杂度:O(1)

稳定性:不稳定

快速排序

算法

找基准:定义low为0号下标,high为length-1号下标。将low号下标的元素放到tmp中,从后往前找比tmp中元素小的数据,放到low号的位置;从前往后找比tmp元素大的数据,放到high号下标的位置。当low和high相遇了,把tmp放进去。这个地方就是基准的位置。基准的左边都比基准小,基准的右边都比基准大。pivot【一趟快速排序】--》给个区间[low,high],递归找其他的基准!

 

    public static int partion(int[] array, int start, int end) {
        int tmp = array[start];
        while (start < end) {
            while ((start < end) && array[end] >= tmp) {  //9 3 2 9 10
             end--;
            }
            if (start >= end) {
               array[start] = tmp;
               break;
            } else {
                array[start] = array[end];
            }

            while ((start < end) && array[start] <= tmp) {  //9 3 2 9 10
               start++;
            }
            if (start >= end) {
                array[start] = tmp; //
                break;
            } else {
                array[end] = array[start];
            }

        }
        return start;
    }

    public static void quick(int[] array, int low, int high) {
        //递归的终止条件:只有一个元素
        if (low >= high) { //=zuo  >you
            return;
        }
        //1.写一个函数把待排序序列分为两部分
        int pivot = partion(array, low, high); //low和high是局部变量
        //开始递归 左右
        quick(array, low, pivot-1);
        quick(array, pivot+1, high);
    }

    public static void quickSort(int[] array) {
        quick(array, 0, array.length-1);
    }

快速排序的分析

时间复杂度:O(nlog2n)

最好时间复杂度:

最坏时间复杂度:O(n^2)[数据有序的情况]

空间复杂度:O(log2n) [左树高度]

稳定性:不稳定

快速排序的优化

优化1:

当快速排序的过程中数据有可能逐渐趋于有序,对于直接插入排序来说,数据越有序越快。O(n)-》n-》小。当在排序的过程中,某个区间的数据量很小时,阈值-》100 用直接插入排序

public class QuickSortbetter1 {
    public static int partion(int[] array, int start, int end) {
        int tmp = array[start];
        while (start < end) {
            while ((start < end) && array[end] >= tmp) {  //9 3 2 9 10
                end--;
            }
            if (start >= end) {
                array[start] = tmp;
                break;
            } else {
                array[start] = array[end];
            }

            while ((start < end) && array[start] <= tmp) {  //9 3 2 9 10
                start++;
            }
            if (start >= end) {
                array[start] = tmp; //
                break;
            } else {
                array[end] = array[start];
            }

        }
        return start;
    }

    public static void insertSort2(int[] array, int low, int high) {
        int j = 0;
        for (int i = low+1; i <= high; i++) {
            int tmp = array[i];
            for (j = i-1; j >= low; j--) {
                if (array[j] > tmp) {
                    array[j+1] = array[j];
                } else {
                    break;
                }
            }
            array[j+1] = tmp;
        }
    }

    public static void quick(int[] array, int low, int high) {
        //递归的终止条件
        if (low >= high) {
            return;
        }

        if (high-low+1 < 100) {
            insertSort2(array, low, high);
            return;
        }
        //1.写一个函数把待排序序列分为两部分
        int pivot = partion(array, low, high); //low和high是局部变量
        //开始递归 左右
        quick(array, low, pivot-1);
        quick(array, pivot+1, high);
    }

    public static void quickSortBetter1(int[] array) {
        quick(array, 0, array.length-1);
    }

 

优化2:

如果是1 2 3 4 5 6 7 8 9 10 找基准退化为冒泡排序了

分治算法-》快排--》最好的情况就是将待排序序列均匀的分割。

如果想要1 2 3 4 5 6 7 8 9 10 均匀的分割:三数取中法 9/2=4。。取三位数中间的中位数作为基准

即就是每次取待排序序列low和high的中位数,array[mid] <= array[low] <= array[high]。即要的结果就是就是low下标的元素是三个数的中位数。

 

public class QuickSortBetter2 {
    public static int partion(int[] array, int start, int end) {
        int tmp = array[start];
        while (start < end) {
            while ((start < end) && array[end] >= tmp) {  //9 3 2 9 10
                end--;
            }
            if (start >= end) {
                array[start] = tmp;
                break;
            } else {
                array[start] = array[end];
            }

            while ((start < end) && array[start] <= tmp) {  //9 3 2 9 10
                start++;
            }
            if (start >= end) {
                array[start] = tmp; //
                break;
            } else {
                array[end] = array[start];
            }

        }
        return start;
    }

    public static void swap(int[] array, int low, int high) {
        int tmp = array[low];
        array[low] = array[high];
        array[high] = tmp;
    }

    public static void ThreeNumOfMiddle(int[] array, int low, int high) {
        //array[mid] <= array[low] <= array[high];
        int mid = (low+high)/2;
        if (array[mid] > array[high]) {
            swap(array, mid, high);
        }
        if (array[mid] > array[low]) {
            swap(array, mid, low);
        }
        if (array[low] > array[high]) {
            swap(array, low, high);
        }
    }


    public static void quick(int[] array, int low, int high) {
        //递归的终止条件
        if (low >= high) {
            return;
        }

        ThreeNumOfMiddle(array, low, high);
        //1.写一个函数把待排序序列分为两部分
        int pivot = partion(array, low, high); //low和high是局部变量
        //开始递归 左右
        quick(array, low, pivot-1);
        quick(array, pivot+1, high);
    }

    public static void quickSort(int[] array) {
        quick(array, 0, array.length-1);
    }

优化3: 

聚集相同基准元素法。

快速排序非递归

public static int partion(int[] array, int start, int end) {
    int tmp = array[start];
    while (start < end) {
        while ((start < end) && array[end] >= tmp) {  //9 3 2 9 10
            end--;
        }
        if (start >= end) {
            array[start] = tmp;
            break;
        } else {
            array[start] = array[end];
        }

        while ((start < end) && array[start] <= tmp) {  //9 3 2 9 10
            start++;
        }
        if (start >= end) {
            array[start] = tmp; //
            break;
        } else {
            array[end] = array[start];
        }

    }
    return start;
}

public static void quick(int[] array, int low, int high) {
  int pivot = partion(array, low, high);
  Stack<Integer> stack = new Stack<>();

  if (pivot > low+1) { //左边有两个元素可以入栈
      stack.push(low);
      stack.push(pivot-1);
  }
  if (pivot < high-1) { //右边有两个元素可以入栈
      stack.push(pivot+1);
      stack.push(high);
  }

  while (!stack.empty()) {
      high = stack.pop();
      low = stack.pop();
      pivot = partion(array, low, high);
      if (pivot > low+1) { //左边有两个元素可以入栈
          stack.push(low);
          stack.push(pivot-1);
      }
      if (pivot < high-1) { //右边有两个元素可以入栈
          stack.push(pivot+1);
          stack.push(high);
      }
  }

}

public static void quickSort(int[] array) {
    quick(array, 0, array.length-1);
}

 

归并排序

算法

归并排序采用了分治的算法。分就是将一些问题分为小的问题进行递归求解,而治的则将分的阶段解得的各答案修补到一起。给定一个待排序序列时,我们应该分为两大步:1.把它划分为一个一个有序的序列 2.进行二路归并(两个有序表合并为一个有序表)。

    //合并递归完的数组
    public static void merge1(int[] array,int low,int mid,int high) {

        int s1 = low;
        int s2 = mid+1;

        int[] tmpArr = new int[high-low+1];  //需要重新申请一个空间存储数组元素
        int i = 0;//tmpArr的数组下标
        //当两个归并段都有数据的时候
        while (s1 <= mid && s2 <= high) {
            //如果是小于,那么就不稳定了
            if(array[s1] <= array[s2]) {
                tmpArr[i++] = array[s1++];
            }else {
                tmpArr[i++] = array[s2++];
            }
        }
        //S1还有数据的情况下
        while (s1 <= mid) {
            tmpArr[i++] = array[s1++];
        }
        //s2还有数据的情况下
        while (s2 <= high) {
            tmpArr[i++] = array[s2++];
        }
        //tmpArr里面存放的是有序的数据
        //将tmpArr里面存放的有序的数据,放回到array里面
        for (int j = 0; j < tmpArr.length; j++) {
            array[low+j] = tmpArr[j];
        }
    }

    //进行递归:分
    public static void mergeSortInternal(int[] array,int low,int high) {
        if(low >= high) { //递归的终止条件
            return;
        }
        //以mid区分前后两段 分段递归
        int mid = (low+high)/2;
        mergeSortInternal(array,low,mid);
        mergeSortInternal(array,mid+1,high);
        //把递归完的数组进行合并
        merge1(array,low,mid,high);
    }

    public static void mergeSort1(int[] array) {
        mergeSortInternal(array,0,array.length-1);
    }

归并排序的分析 

分治下来就像一颗二叉树,时间复杂度O(n*log2n);

重新申请了一块空间,空间复杂度是O(n)

是一个稳定的排序

归并排序非递归

public static void mergeSort(int[] array) {

    //进行分组归并
    for (int i = 1; i < array.length; i *= 2) {
        merge(array,i);
    }
}
//gap代表每个归并段的数据
public static void merge(int[] array,int gap) {
    int[] tmpArr = new int[array.length];
    int k = 0;//下标

    int s1 = 0; //第一组的开头
    int e1 = s1+gap-1; //第一组的结尾
    int s2 = e1+1; //第二组的开头
    int e2 = s2+gap-1 < array.length ? s2+gap-1:array.length-1; //第二组的结尾

    //两个归并段都有数据
    while (s2 < array.length) {

        while (s1 <= e1 && s2 <= e2) {
            if(array[s1] <= array[s2]) {
                tmpArr[k++] = array[s1++];
            }else {
                tmpArr[k++] = array[s2++];
            }
        }

        while (s1 <= e1) {
            tmpArr[k++] = array[s1++];
        }

        while (s2 <= e2) {
            tmpArr[k++] = array[s2++];
        }

        s1 = e2+1;
        e1 = s1+gap-1;
        s2 = e1+1;
        e2 = s2+gap-1 < array.length ? s2+gap-1:array.length-1;
    }
    //判断是不是还有一个归并段,且这个归并段一定是s1那个段,直接小于e1可能会越界
    while (s1 <= array.length-1) {
        tmpArr[k++] = array[s1++];
    }

    for (int i = 0; i < tmpArr.length; i++) {
        array[i] = tmpArr[i];
    }
}

不是所有排序都是基于比较的排序,比如计数排序、基数排序,桶排序等。 

2019年的最后一天,在这里许个愿望吧。希望我在2020这个美好的一年里,能拿到一个满意的offer,冲鸭白!为了梦想,不断前行。2019再见,2020你好嘻嘻嘻O(∩_∩)O哈哈~

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值