实现七大排序算法

237 阅读7分钟

产生一个随机整形数组

使用java.util.HashSet集合元素不重复的特点来避免产生重复的元素

实现代码

public static int[] getInt(int size) {
        Set s = new HashSet();
        int c = 0;
        while (c < size) {
            int num = (int)(Math.random()*size*100);
            s.add(num);
            c = s.size();
        }
        Object[] oArray = s.toArray();
        int[] intArray = new int[size];
        int i = 0;
        for (Object o:oArray) {
            int tem = (int)o;
            intArray[i++] = tem;
        }
        return intArray;

算法的基本概念

算法稳定性

元素a=b,在排序前元素a在元素b的前面,排序后如果a仍然位于b的前面则算法具有稳定性,反之则不稳定。

原地算法(in-place)

在计算机科学中,一个原地算法(in-place algorithm)基本上不需要额外辅助的数据结构,然而,允许少量额外的辅助变量来转换数据的算法。当算法运行时,输入的数据通常会被要输出的部分覆盖掉。不是原地算法有时候称为非原地(not-in-place)或不得其所(out-of-place)。–摘自维基百科

冒泡排序 (Bubble Sort)

算法思路

重复扫描数列,每次比较相邻两个元素的大小,小的元素放在左侧,经过一次扫描,数列最大的数位于数列的尾部,视为该元素有序,重复以上工作,直至整个数列有序。

意思就是算法只需要用到O(1)的额外内存空间就可进行

算法描述

  1. 重复扫描整个数列,扫描次数为数列长度-1。
  2. 每次扫描(数列的长度-有序元素的个数-1)次。
  3. 比较两个相邻元素的大小,下标小的存小的元素,下标大的放大的元素。

实现代码

int[] a = RandomNum.getInt(5); //随机数数量
        int len = a.length;
        for (int i = 0; i < len - 1; i++) {
            for(int j = 0; j < len - 1 - i; j++) {
                if(a[j] > a[j+1]) {
                    int z = a[j];
                    a[j] = a[j+1];
                    a[j+1] = z;
                }
            }
        }

时间复杂度

  1. 最好的情况

数列正序,需要判断一轮扫描都没有进行交换,则说明数列排序完毕,那么只需要扫描一遍即可,时间复杂度为O(n)

  1. 最坏的情况

数列反序,需要扫描数列n-1次,每次扫描需要比较n-i次(1<=i<=n-1),时间复杂度为O(n^2)

算法稳定性

如果两个元素相等且相邻,两个元素不会进行对调,即使不相邻,经过多次交换最终两个相同的元素前后顺序也不会交换,所以冒泡算法是一种稳定排序算法

选择排序(SelectionSort)

算法思路

将数列分为已排序的数列和未排序的数列,每次扫描未排序数列选出最小(最大)的元素,放入已排序数列尾部

算法描述

  1. 重复扫描数组n-1次
  2. 每次选出数组最小/最大的元素,置于有序区末尾

实现代码

System.out.println("排序前");
        soutIntArray(a);//输出队列元素
        int len = a.length;
        //外层扫描
        for (int i=0; i < len - 1; i++) {
            int temp = a[i];
            int index = i;
            //内层扫描,记录最小元素的值和下标
            for (int j = i+1; j < len; j++) {
                if (a[j] < temp) {
                    temp = a[j];
                    index = j;
                }
            }
            //与当前有序区的元素交换位置
            a[index] = a[i];
            a[i] = temp;
        }
        System.out.println("排序后");
        soutIntArray(a);

时间复杂度

不管数列如何,选择排序需要扫描n-1次,每次扫描需要比较n-i次(0<=i<=n-1),所以时间复杂度总等于O(n^2)

算法稳定性

如果有两个相同的a、b元素,并且有序区还在a、b元素之前,那么b元素会被调换到a元素之前,所以选择排序不是一种稳定的排序算法

插入排序(Insertion Sort)

算法分析

插入排序通常采用in-place排序,在实现的过程中,因为是从后往前扫,就需要反复的移动已排序的元素,为新插入的元素空出位置。

算法描述

  1. 选定第一个元素,并视为有序
  2. 从无序数列中取出下一个元素,在有序数列中从后往前扫描
  3. 如果新的元素小于比较的元素,则将比较元素后移
  4. 重复步骤3,直到找到比新元素小的元素并插入到比较元素的前方,或者当比较元素下标小于0时,直接插入下标0处。
  5. 重复2、3、4步骤,直到无序数列为空

代码实现

        int len = a.length;
        //默认第一个元素为有序,扫描无序数列
        for (int i = 1; i < len; i++) {
            int temp = a[i];
            //从后往前扫描有序序列
            for (int j = i - 1; j < len && j >= 0; j--) {
            	if(j < 0) {
                	a[j] = temp;
                }
                //新的元素比比较元素小,比较元素后移
                if (a[j] > temp) {
                    a[j + 1] = a[j];
                    a[j] = temp;
                    continue;
                }
                //新的元素比比较的元素大则插入其后方
                a[j + 1] = temp;
                break;
            }
        }

时间复杂度

  1. 最好的情况

当数列为正序,每次只需要比较一次即可完成一次插入,总共需要比较n-1次,因此时间复杂度为O(n) 2. 最坏的情况

当数列为反序,每次扫描有序序列需要扫描n-i(i为无序元素个数),总共需要为n-1个无序元素进行排序,因此时间复杂度为O(n^2) 3. *平均情况

无序元素a[k]可能插入的位置有[0,k]个位置,也就是说每个位置被插入的概率均为1/k.

插入无序元素a[k]可能需要比较的次数为[1,k-1]次,平均比较次数为(1+2+3+4+......+k-2+k-1)/k

适用场景

数据量小的时候,或者数列基本有序的时候

希尔排序(Shell Sort)

写入排序是插入排序的改进版,对于中等规模的数据的排序性能表现不错。

算法分析

在插入排序的基础上,新增增量这个概念。对整个数列进行逻辑上分组,分别对每个分组进行插入排序,进行插入排序后,每个分组就为有序数列。缩小增量,继续划分分组,直至最后增量为1,因为此时数组整体有序,因此插入排序的效率较高,同理进行插入排序,完成希尔排序。

增量:按照增量作为间隔距离,对数量进行分组

算法描述

  1. 确定增量i(1<=i<=n/2)
  2. 对数列按照增量进行分组
  3. 对每个分组进行插入排序
  4. 增量缩小一半
  5. 重复1、2、3、4直至增量为1

实现代码

        //根据增量对原数组进行逻辑分组
        while (increment >= 1) {
            for (int i = 0; i < len; i++) {
                for (int j = i + increment; j < len; j += increment) {
                    int temp = a[j];
                    //对每个分组进行插入排序
                    for (int k = j - increment; k < len && k >= 0; k -= increment) {
                        if (a[k] < temp) {
                            a[k+increment] = a[k];
                            a[k] = temp;
                            continue;
                        }
                        a[k+increment] = temp;
                        break;
                    }
                }
            }
            increment /= 2;
        }

归并排序(Merge Sort)

采用的是分治的思想,将已有的序列进行分组,分到剩余一个元素,再子序列合并,合并的过程中进行排序。也就是说,先让子序列有序,再使得母序列有序

算法分析

对序列进行分组,直至每个分组剩余一个元素,实现采用递归的方式。完成分组后需要合并分组,合并的时使用两个指针并且需要一个额外的数组保存合并后的序列,两个指针分别指向左右分组的起点,比较两个元素的大小,小的优先插入,当其中一个指针跑到尾部,结束合并,将另一组未写入的元素按顺序写入新的数组中,然后将新的数组的元素值复制给原数组。

算法描述

分组函数

  1. 判断左右界限是否相同,相同则返回
  2. 计算下标中位数,mid=L+((R-L)>>1)
  3. 递归调用本函数,左侧分组的L=L、R=mid。右侧分组的L=mid+1、R=R
  4. 调用合并函数

合并函数

  1. 创建额外的数组,数组大小为(R-L+1)
  2. 使用两个指针指向左右分组的首位元素
  3. 比较两个指针指向的元素的大小,小的数插入额外的数组中,并且指针后移一位
  4. 重复3,直至其中一个分组为空
  5. 将还未写完的分组元素按顺序写入额外数组
  6. 将额外的数组中的元素按顺序写入原数组中

代码实现

    //归并排序
    //将数列拆分至单个元素
    public static void sort(int a[], int left, int right) {
        if (left >= right) {
            return;
        }
        int mid = left + ((right - left) >> 1);
        sort(a, left, mid);
        sort(a, mid + 1, right);
        merge(a, left, mid, right);
    }
    //合并分组
    public static void merge(int a[], int left, int mid, int right) {
        //存放合并后的分组
        int temp[] = new int[right - left + 1];
        //指向两个分组的指针
        int p1 = left;
        int p2 = mid + 1;
        //指向额外数组
        int index = 0;
        //选择两个分组中较小的元素插入额外数组
        while (p1 <= mid && p2 <= right) {
            temp[index++] = a[p1] <= a[p2] ? a[p1++] : a[p2++];
        }
        //将还未写入的分组写入额外数组
        while (p1 <= mid) {
            temp[index++] = a[p1++];
        }
        while (p2 <= right) {
            temp[index++] = a[p2++];
        }
        //将额外数组中的内容按照对应的下标写入原来的数组
        for (index = 0;left <= right;index++) {
            a[left++] = temp[index];
        }
    }

时间复杂度

对于归并排序,无论数组是哪种状态,时间复杂度都不会变。都是O(log2n)

算法稳定性

上述的实现代码中,选择分组元素进入额外数组时,使用了<=符号,保证了算法的稳定性

快速排序(Quick Sort)

算法分析

快速排序同样也是采用了分治的思想,通过选取基准值,将数列分割成两个分组,比基准小的放在左侧,比基准大的放在右侧,对两个分组继续进行排序,直到最终对整个序列完成排序

算法描述

分组函数

  1. 调用排序函数,获得基准值
  2. 将左侧分组代入到排序函数中
  3. 将右侧分组代入到排序函数中

排序函数

  1. 选取基准值,默认选取组内的第一个元素作为基准值
  2. 使用a、b指针分别指向数列的首部和尾部
  3. 从b指针开始往前扫描碰到比基准值大的元素,尾部指针前移一位。遇到比基准值小的则与a指针交换值,并且a指针后移一位
  4. a指针往后扫描,遇到比基准值小的值后移一位。遇到比基准值大的数则与b指针交换数值,并且b指针前移一位
  5. 重复3、4直至a、b指针相遇,此时会发现基准值左侧的元素均小于它,右侧的元素均大于它
  6. 返回基准值

代码实现

    //快速排序
    //对两侧数组继续进行排序
    public static void quickSort(int a[], int left, int right) {
        //当元素个数为1时跳出排序
        if (left > right) {
            return;
        }
        int index = partition(a,left,right);
        //对左右侧分组进行排序
        quickSort(a,left,index-1);
        quickSort(a,index+1,right);
    }
    //已基准值为准,将小于基准值的元素放到左侧,大于基准值的元素放到右侧
    public static int partition(int a[], int left, int right) {
        //记录基准值大小
        int pivot = a[left];
        //确定左右指针
        int p1 = left;
        int p2 = right;
        while (p1 != p2) {
            while (p1 != p2) {
                //扫描右侧指针,如果比基准值大,则指针向前迁移
                //如果比基准值小,交换左右指针的值,左指针后移
                if (a[p2] <= pivot) {
                    a[p1++] = a[p2];
                    break;
                }
                p2--;
            }
            while (p1 != p2) {
                if (a[p1] >= pivot) {
                    a[p2--] = a[p1];
                    break;
                }
                p1++;
            }
        }
        //两指针相遇后将基准值赋值到相遇的位置,根据上面算法原理,左侧的值都比基准值小,右侧的值都比基准值大
        a[p1] = pivot;
        return p1;
    }

时间复杂度

  1. 一般情况

  2. 最差情况

每次基准值选中的都是最大或者最小的值,此时时间复杂度与冒泡相同O(n^2)

算法稳定性

快速排序牺牲了算法的稳定性来换取更高的执行效率,当一个数列中所有元素相同时,如果对于相同的元素均不调换的话,时间复杂度会变为O(n^2)。

堆排序

满二叉树:如果一个二叉树每层的节点数均为最大值,也就是说如果二叉树高度为k,那么该二叉树的节点个数为2^k-1

完全二叉树:一颗高度为k的二叉树,除了第k层外,其余层数节点数均为最大值,且最后一层节点从左到右依次编号,不可中断

:堆是具有特殊性的完全二叉树。每个节点的值都大于左右孩子的完全二叉树被称为大顶堆,反之被称为小顶堆

映射到数组中

二叉树映射到数组中 设当前节点的编号为i则得到 a[2i+1] = 左节点编号 a[2i+2] = 右节点编号

算法分析

需要使用到数据结构大顶堆/小顶堆,一般正序用到大顶堆、降序用到小顶堆。(示例使用大顶堆)首先构造大顶堆,将数列放入数组中,从编号最大的不为叶子节点的节点开始,按照该节点最大的原则进行调整,每次调整可能会使子树失去大顶堆性质,因此需要对子树进行递归操作。构造大顶堆后,根节点为当前数列最大的数,将其与最后一个节点交换,交换完后当前状态可能违反大顶堆的性质,此时需要进行调整使其符合大顶堆性质,在调整过程中,需要注意此时有序区+1,树大小-1、调整完毕后,按照该逻辑继续进行,直到整个数列有序

算法逻辑

//构造大顶堆方法

  1. 获取编号最大的不为叶子结点的节点p的编号=(len/2)向下取整 len/2由子节点公式推得,设当前节点编号为i,左孩子节点=2i+1、右孩子节点=2i+2。取数组长度len也就是最后一个叶子节点的编号,求其父节点即为1中要求的节点p
  2. 从p节点依次往前调用调整方法

//调整树方法 对树进行调整,使其符合大顶堆性质

  1. 通过获取到的节点编号计算出左右子树的编号
  2. 创建额外变量max用于存储三个节点之中值最大的编号,首先将父节点编号的值赋予max
  3. 判断如果左子树的值大于max指向的值,记录左子树编号到max中
  4. 判断如果右子树的值大于max指向的值,记录右子树编号到max中
  5. 判断如果max不等于父节点编号,则调用交换值方法交换两编号中的值,并对交换后的子节点进行递归调用

//交换两节点的值方法

//堆排序方法 交换根节点与对应的节点,调整后继续进行交换,直至完成排序

  1. 调用构造大顶堆方法,构件大顶堆
  2. 交换根节点与对应的节点i(len<=i<1),此时被调换的节点属于有序区,所以需要将树的逻辑长度减一
  3. 调用调整树方法,调整交换后的树,使其重新符合大顶堆性质
  4. 重复执行1、2直至完成排序

注意点

在构建完大根堆之后交换根节点和对应的节点后,需要将树的逻辑大小减一,因为此时被调换节点已经属于有序区,重新调整树使其符合大根堆性质时,需要排除有序区的元素。

实现代码

    //堆排序
    //交换元素
    public static void swap (int[] a, int father, int child) {
        int temp = a[father];
        a[father] = a[child];
        a[child] = temp;
    }
    //构件大顶堆
    public static void buildMaxHeap(int a[], int len) {
        //index指向最后一个非叶子节点的节点
        int index = (a.length - 1)/2; //因为java中基本数据类型int中出现的小数点直接舍去,相当于向下取整,符合算法逻辑
        //已index为准,依次往前调用保持大顶堆性质的方法,构造出大顶堆
        while (index >= 0) {
            maxHeapify(a,index--,len);
        }
    }
    //维持大顶堆性质
    public static void maxHeapify(int a[], int father,int len) {
        //根据父节点算出左右节点的编号
        int left = (father*2 + 1);
        int right = (father*2 + 2);
        //找出三个节点中最大节点的编号
        int max = father;
        if (left <= len && a[left] > a[max]) {
            max = left;
        }
        if (right <= len && a[right] > a[max]) {
            max = right;
        }
        //如果最大值不为父节点,交换两节点
        if (father != max) {
            swap(a,father,max);
            //交换后子树可能失去大顶堆性质,需要再次递归调用本方法
            maxHeapify(a,max,len);
        }
    }
    //堆排序
    public static void heapSort(int a[]) {
        int len = a.length - 1;
        buildMaxHeap(a, len);
        //交换根节点与对应的节点n-1次
        for (int i = len; i > 0; i--) {
            swap(a,0,i);
            //交换一次,有序区+1,树的长度-1
            maxHeapify(a,0, --len);
        }
    }