Algorithm Foundation 之 排序

1. Sort

1.1 Insertion Sort

1.1.1 Direct Insertion Sort 直接插入排序

通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

  1. 将第一个数和第二个数排序,然后构成一个有序序列
  2. 将第三个数插入进去,构成一个新的有序序列。
  3. 对第四个数、第五个数……直到最后一个数,重复第二步。

时间复杂度:最好:O(n),平均:O( n 2 n^2 n2) ,最差:O( n 2 n^2 n2)

空间复杂度:O(1)

是否稳定:稳定

适用于基本有序的排序表和数量不大的排序表

public static int[] sort(int[] array) {
    int current = 0;
    // starts with the second number and its index is 1
    int j = 0;
    for (int i = 1; i < array.length; i++) {
      j = i - 1;
      current = array[i];
      // when the current value is smaller than the previous one, move value back.
      while (j >= 0 && current < array[j]) {
        array[j + 1] = array[j];
        --j;
      }

      // when ends the while loop, variable j has executed the self-minus operator.
      array[j + 1] = current;
    }

    return array;
}

1.1.2 Shell Sort

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

在分组内构建有序排序。

对直接插入排序的改进,又称为缩小增量排序。

  1. 将数的个数设为n,取奇数k=n/2,将下标差值为k的书分为一组,构成有序序列。
  2. 再取k=k/2 ,将下标差值为k的书分为一组,构成有序序列。
  3. 重复第二步,直到k=1执行简单插入排序。

时间复杂度:不固定;

空间复杂度:O(1);

是否稳定:不稳定;

代码注意点:移位时,始终用的都是 groups,即和下一个分组中的相同位置的元素交换数据

public static int[] shellSort(int[] array) {
  // 分组
  for (int groups = array.length >> 1; groups != 0; groups = groups >> 1) {
    // 组内挪步
    for (int stride = 0; stride < groups; stride++) {
      // 组间挪步
      for (int i = stride + groups; i < array.length; i += groups) {
        int current = array[i];
        int j = i - groups;
        for (; j >= 0 && current < array[j]; j -= groups) {
          // 挪位
          array[j + groups] = array[j];
        }
        // 找到合适的位置,插入数据
        array[j + groups] = current;
      }
    }
  }
  return array;
}

1.2 Selection Sort

1.2.1 Simple Selection Sort

每次从待排序列中选取最小的元素插入到已排序序列的最后一个位置。

常用于取序列中最大最小的几个数。

  1. 遍历整个序列,将最小的数放在最前面。
  2. 遍历剩下的序列,将最小的数放在最前面。
  3. 重复第二步,直到只剩下一个数。

时间复杂度:最好:O( n 2 n^2 n2);平均:O( n 2 n^2 n2);最差:O( n 2 n^2 n2);

空间复杂度:O(1)

是否稳定:否

public static int[] selectionSort(int[] array) {
    for (int i = 0; i < array.length; i++) {
      int swap = array[i];
      int index = i;
      for (int j = i + 1; j < array.length; j++) {
        if (array[j] < swap) {
          swap = array[j];
          index = j;
        }
      }
      array[index] = array[i];
      array[i] = swap;
    }
    return array;
}

1.2.2 Heap Sort 堆排序

利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。

将 L[1 … n] 看成是一颗完全二叉树的顺序存储结构。

  1. 将序列构建成大顶堆。
  2. 将根节点与最后一个节点交换,然后断开最后一个节点。
  3. 重复第一、二步,直到所有节点断开。
public class HeapSort {
    private int[] array;
    public HeapSort(int[] arr) {
        this.array = arr;
    }

    /**
     * 堆排序的主要入口方法,共两步。
     */
    public void sort() {
        /*
         *  第一步:将数组堆化
         *  beginIndex = 第一个非叶子节点。
         *  从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
         *  叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
         */
        int len = array.length - 1;
        int beginIndex = (len - 1) >> 1;
        for (int i = beginIndex; i >= 0; i--) {
            maxHeapify(i, len);
        }
        /*
         * 第二步:对堆化数据排序
         * 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,同时遍历长度 - 1。
         * 然后从新整理被换到根节点的末尾元素,使其符合堆的特性。
         * 直至未排序的堆长度为 0。
         */
        for (int i = len; i > 0; i--) {
            swap(0, i);
            maxHeapify(0, i - 1);
        }
    }

    private void swap(int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    /**
     * 调整索引为 index 处的数据,使其符合堆的特性。
     *
     * @param index 需要堆化处理的数据的索引
     * @param len 未排序的堆(数组)的长度
     */
    private void maxHeapify(int index, int len) {
        // 左子节点索引
        int li = (index << 1) + 1;
        // 右子节点索引
        int ri = li + 1;
        // 子节点值最大索引,默认左子节点。
        int cMax = li;
        // 左子节点索引超出计算范围,直接返回。
        if (li > len) {
            return;
        }
        // 先判断左右子节点,哪个较大。
        if (ri <= len && array[ri] > array[li]) {
            cMax = ri;
        }
        if (array[cMax] > array[index]) {
            // 如果父节点被子节点调换,
            swap(cMax, index);
            // 则需要继续判断换下后的父节点是否符合堆的特性。
            maxHeapify(cMax, len);
        }
    }

    public static void main(String[] args) {
        Random random = new Random();
        int[] data = new int[20];
        for (int i = 0; i < data.length; i++) {
            data[i] = random.nextInt(100);
        }

        new HeapSort(data).sort();
        System.out.println(Arrays.toString(data));
    }
}
1.2.2.1 应用 (重点)

如果只想得到一个序列中第 k (k >= 5) 个最小元素之前的部分排序序列,最好采用堆排序。

在 基于比较的排序方法中:

  1. 插入排序、快速排序和归并排序,只有在将元素全部排完序后,才能得到前 k 小的元素序列,算法的效率不高。
  2. 冒泡排序、堆排序和简单选择排序可以,因为它们在每一趟中都可以确定一个最小的元素。采用堆排序最合适,对于 n 个元素的序列,建立初始堆的时间不超过 4n,取得第 k 个最小元素之前的排序序列所花时间为 k l o g 2 n klog_2^n klog2n,总时间为 4 n + k l o g 2 n 4n + klog_2^n 4n+klog2n,冒泡和简单选择排序完成此功能所花时间为 kn,当 $ k \ge 5$时,通过比较可以得出堆排序最优。

引申:只需要得到前 k 小元素的顺序排列可采用的排序算法有冒泡排序、堆排序和简单排序。

时间复杂度:最好: n l o g 2 n nlog_{2}n nlog2n;平均: n l o g 2 n nlog_{2}n nlog2n;最差: n l o g 2 n nlog_{2}n nlog2n

空间复杂度:O(1);

是否稳定:否;

1.3 交换排序

1.3.1 冒泡排序

基本不使用。

public void bubbleSort(int[] a){
    int length=a.length;
    int temp;
    for(int i=0;i<a.length;i++){
      for(int j=0;j<a.length-i-1;j++){
        if(a[j]>a[j+1]){
          temp=a[j];
          a[j]=a[j+1];
          a[j+1]=temp;
        }
      }
    }
}

时间复杂度:最好 O( n n n);平均:O( n 2 n^2 n2);最差:O( n 2 n^2 n2);

空间复杂度:O(1);

是否稳定:是;

1.3.2 快速排序

一般面试多考察此算法。

基本思想是基于分治法,选取一个元素作为枢轴 pivot,将元素划分为大于和小于枢轴两部分,递归上面的步骤。

关键字:枢轴 pivot;

在快速排序算法中,并不产生有序子序列,但每一趟排序后将一个元素(枢轴元素)放到其最终的位置上。

public class QuickSort {

    private static int[] quickSort(int[] arr, int low, int high) {
        if (low < high) {
            //将数组分为两部分
            int pivotPosition = partition(arr, low, high);
            //递归排序左子数组
            quickSort(arr, low, pivotPosition - 1);
            //递归排序右子数组
            quickSort(arr, pivotPosition + 1, high);
        }
        return arr;
    }

    private static int partition(int[] arr, int low, int high) {
        // 枢轴记录
        int pivot = arr[low];
        while (low < high) {
            // 从后向前找,找到一个比枢轴小的值
            while (low < high && arr[high] >= pivot) {
                --high;
            }
            // 交换比枢轴小的记录到左端
            arr[low] = arr[high];
            // 从前向后找,找到一个比枢轴大的值
            while (low < high && arr[low] <= pivot) {
                ++low;
            }
            // 交换比枢轴大的记录到右端
            arr[high] = arr[low];
        }
        // 扫描完成,枢轴到位
        arr[low] = pivot;
        // 返回的是枢轴的位置
        return low;
    }

    public static void main(String[] args) {
        Random random = new Random();
        int[] data = new int[20];
        for (int i = 0; i < data.length; i++) {
            data[i] = random.nextInt(100);
        }

        data = quickSort(data, 0, data.length - 1);
        System.out.print(Arrays.toString(data));
    }
}

1.3.3 应用(重点)

1.3.3.1 问题一 双向起泡

编写一个双向冒泡排序算法,在正反两个方向交替进行扫描,即第一趟把关键字最大的元素放在序列的最后面,第二趟把关键字最小的元素放在序列的最前面,重复。

奇数趟时,从前向后比较相邻元素的关键字,遇到逆序即交换,直到把序列中关键字最大的元素移动到序列尾部。偶数趟时,从后向前比较相邻元素的关键字,遇到逆序即交换,直到把序列中关键字最小的元素移动到序列前端。

public static int[] doubleBubleSort(int[] array) {
    int low = 0;
    int high = array.length - 1;
    boolean flag = true;
    // flag 为 false 时,说明已没有逆序元素,终止
    // 注意体会两个更新上下界操作
    while (low < high && flag) {
      // 每趟初始置 flag 为 false
      flag = false;

      for (int i = low; i < high; i++) {
        if (array[i] > array[i + 1]) {
          // 交换
          int temp = array[i];
          array[i] = array[i + 1];
          array[i + 1] = temp;

          flag = true;
        }
      }
      // 更新上界
      --high;
      for (int i = high; i > low; i--) {
        if (array[i] < array[i - 1]) {
          // 交换
          int temp = array[i];
          array[i] = array[i - 1];
          array[i - 1] = temp;

          flag = true;
        }
      }
      // 更新下界
      ++low;
    }
    return array;
}
1.3.3.2 问题二 奇数移到偶数前面
  1. 已知线性表按顺序存储,且每个元素都是不相同的整数型元素,设计把所有奇数移动到所有偶数前边的算法(要求时间最少,辅助空间最少)

采用基于快速排序的划分思想来设计算法

只需要一个遍历即可,其时间复杂度是 O(n),空间复杂度是 O(1)。

基本思想:从前向后找到一个偶数元素 L(i),在从后向前找到一个奇数元素 L(j),交换,重复这个过程,直到 i 大于 j。

public static int[] exchange(int[] array) {
  	int i = 0;
  	int j = array.lenth;
  	while (i < j) {
      	while (array[i] % 2 == 1) {
          	++i;
        }
      	while (array[j] % 2 == 0) {
          	--j;
        }
      	// 注意这个条件判断
      	if (i < j) {
          	int temp = array[i];
            array[i] = array[j];
            array[j] = temp;
        }
      	++i;
      	--j;
    } 
}
1.3.3.3 问题三 找出第 k 小的元素(重点)

编写算法,使之能够在数组 L[1 … n] 中找出第 k 小的元素(即从小到大排序后处于第 k 个位置的元素)

方法一:用排序算法,平均时间复杂度至少达到 O( n l o g 2 n nlog_{2}n nlog2n)。

方法二:采用小顶堆,时间复杂度为 O( n + k l o g 2 n n + klog_{2}n n+klog2n)。

方法三:最优的算法,基于快速排序的划分操作。

主要思想:从数组 L[1 … n] 中选择枢轴 pivot (随机地或直接取第一个) 进行和快速排序一样的划分操作后,表 L[1 … n] 被划分为 L[1 … m-1] 和 L[m+1 … n],其中 pivot = L[m] .

讨论 m 与 k 的大小关系:

1) 当 m = k 时,显然 pivot 就是所要寻找的元素,直接返回 pivot 即可。

2) 当 m < k 时,则所要寻找的元素一定落在 L[m + 1 … n]中,从而可以对 L[m+1 … n] 递归地查找第 k - m 小的元素。

3) 当 m > k 时,则所要寻找的元素一定落在 L[1 … m - 1]中,从而可以对 L[1 … m - 1] 递归地查找第 k 小的元素。

该算法的时间复杂度在平均情况下可以达到 O(n),空间复杂度取决于划分的方法。

/**
 * 查找第 k 小的元素
 * @param args
 */
public static int kthElement(int[] array, int low, int high, int k) {
    int pivot = array[low];
    // 下面会修改 low 与 high,在递归时又用到它们
    int lowTemp = low;
    int highTemp = high;
    while (low < high) {
      while (low < high && array[high] >= pivot) {
        --high;
      }
      array[low] = array[high];
      while (low < high && array[low] <= pivot) {
        ++low;
      }
      array[high] = array[low];
    }
    array[low] = pivot;
    // 上面的代码为快速排序中的划分算法
    // 以下就是本算法思想中所述的内容
    if (low == k) {
      return array[low];
    } else if (low > k) {
      return kthElement(array, lowTemp, low - 1, k);
    } else {
      return kthElement(array, low + 1, highTemp, k - low);
    }
}
1.3.3.4 问题四 荷兰国旗问题

设有一个仅由红、白、蓝三种颜色的条块组成的条块序列,请编写一个时间复杂度为 O(n) 的算法,使得这些条块按红、白、蓝的顺序拍好,即排成荷兰国旗图案。

算法思想:顺序扫描线性表,将红色条块交换到线性表的最前面,蓝色条块交换到线性表的最后面,为此设立三个指针,其中,j 为工作指针表示当前扫描的元素,i 以前的元素全部为红色,k 以后的元素全部为蓝色。根据 j 所指示元素的颜色,决定将其交换到序列的前部或者尾部。初始时 i = 0, k = n - 1.

enum Color {
  	RED, WHITE, BLUE
}

public static void flagArrage(Color[] array, int n) {
  	int i = 0;
  	int j = 0;
  	int k = n - 1;
  	while (j <= k) {
      	switch (array[j]) {
          case RED:
            swap(array[i], array[j]);
            ++i;
            ++j;
            break;
          case WHITE:
            ++j;
            break;
          case BLUE:
            swap(array[j], array[k]);
            --k;
            // 这里没有 ++j 语句,以防止交换后 array[j] 仍为蓝色的情况。
        }
    }
}

引申:如果将元素值变成正数、负数和零排序成前面都是负数,接着是 0,最后是正数,也是用同样的方法。

思考:为什么 case RED 时不用考虑交换后 array[j] 仍为红色,而 case BLUE 却需要考虑交换后 array[j] 仍为蓝色?

1.4 归并排序

速度仅次于快排,内存少的时候使用,可以进行并行计算的时候使用。

  1. 选择相邻两个数组成一个有序序列。
  2. 选择相邻的两个有序序列组成一个有序序列。
  3. 重复第二步,直到全部组成一个有序序列。
public class MergeSort {
    public static int[] array;
    public static int[] newArray;

    public static void merge(int[] array, int low, int mid, int high) {
        // array 的两段 array[low ... mid] 和 array [mid + 1 ... high] 各自有序,将它们合并成一个有序表
        for (int i = low; i <= high; i++) {
            newArray[i] = array[i];
        }
        int i = low, j = mid + 1, k = i;
        for (; i <= mid && j <= high; k++) {
            // 比较 newArray 的左右两段中的数据
            if (newArray[i] <= newArray[j]) {
                // 将较小值复制到 array 中
                array[k] = newArray[i++];
            } else {
                array[k] = newArray[j++];
            }
        }
        // 若第一个表未检测完,复制
        while (i <= mid) {
            array[k++] = newArray[i++];
        }
        // 若第二个表为检测完,复制
        while (j <= high) {
            array[k++] = newArray[j++];
        }
    }

    public static void mergeSort(int[] array, int low, int high) {
        if (low < high) {
            int mid = (low + high) >> 1;
            mergeSort(array, low, mid);
            mergeSort(array, mid + 1, high);
            merge(array, low, mid, high);
        }
    }

    public static void main(String[] args) {
        Random random = new Random();
        array = new int[20];
        newArray = new int[array.length];
        for (int i = 0; i < array.length; i++) {
            array[i] = random.nextInt(100);
        }

        mergeSort(array, 0, array.length - 1);
        System.out.print(Arrays.toString(array));
    }
}

二路归并排序:

时间复杂度:最好:O( n l o g 2 n nlog_{2}n nlog2n);平均:O( n l o g 2 n nlog_{2}n nlog2n);最差:O( n l o g 2 n nlog_{2}n nlog2n);

空间复杂度:O(n);

是否稳定:是;

K 路归并排序:

将上面的底数变成 k;

1.5 Radix Sort 基数排序

时间复杂度:最好:O( d ( n + r ) d(n + r) d(n+r));平均:O( d ( n + r ) d(n + r) d(n+r));最差:O( d ( n + r ) d(n + r) d(n+r));d 趟分配和收集,一趟分配需要 O(n),一趟收集需要 O®。

空间复杂度:O®;r 个队列;

是否稳定:是;

public class RadixSort {

    public static int[] radixSort(int[] array) {
        //首先确定排序的趟数;
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) {
                max = array[i];
            }
        }
        int time = 0;
        //判断位数;
        while (max > 0) {
            max /= 10;
            time++;
        }
        //建立10个队列;
        List<ArrayList> queue = new ArrayList<ArrayList>();
        for (int i = 0; i < 10; i++) {
            ArrayList<Integer> queue1 = new ArrayList<Integer>();
            queue.add(queue1);
        }
        //进行time次分配和收集;
        for (int i = 0; i < time; i++) {
            //分配数组元素;
            for (int j = 0; j < array.length; j++) {
                //得到数字的第time+1位数;
                int x = array[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
                ArrayList<Integer> queue2 = queue.get(x);
                queue2.add(array[j]);
                queue.set(x, queue2);
            }
            //元素计数器;
            int count = 0;
            //收集队列元素;
            for (int k = 0; k < 10; k++) {
                while (queue.get(k).size() > 0) {
                    ArrayList<Integer> queue3 = queue.get(k);
                    array[count] = queue3.get(0);
                    queue3.remove(0);
                    count++;
                }
            }
        }
        return array;
    }

    public static void main(String[] args) {
        Random random = new Random();
        int[] data = new int[20];
        for (int i = 0; i < data.length; i++) {
            data[i] = random.nextInt(100);
        }

        data = radixSort(data);
        System.out.print(Arrays.toString(data));
    }
}

2. Summary

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值