排序算法
排序的基本概念
排序的稳定性:
如果待排序的表中有多个关键字相同的字段,经过排序之后这些具有相同关键字的元素之间的相对次序保持不变,则称这种排序为稳定排序;反之,如果具有相同关键字的元素之间的相对次序发生变化则称为不稳定排序。
对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性;而对于稳定的排序算法,必须对算法进行分析从而得到稳定的特性。需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。
例如,对于冒泡排序,原本是稳定的排序算法,如果将记录交换的条件改成A[i] >= A[i + 1],则两个相等的记录就会交换位置,从而变成不稳定的排序算法。
其次,说一下排序算法稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位排序后元素的顺序在高位也相同时是不会改变的。
时间复杂度
计算时间复杂度的方法:
1. 用常数1代替运行时间中的所有加法常数
2. 修改后的运行次数函数中,只保留最高阶项
3. 去除最高阶项的系数
按数量级递增排列,常见的时间复杂度有:
常数阶O(1),对数阶O(log2n),线性阶O(n),
线性对数阶O(nlog2n),平方阶O(n^2),立方阶O(n^3),…,
k次方阶O(n^k),指数阶O(2^n)。
随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。
计算方法:
1. 忽略常数,用O(1)表示
2. 递归算法的空间复杂度=递归深度N*每次递归所要的辅助空间
3. 对于单线程来说,递归有运行时堆栈,求的是递归最深的那一次压栈所耗费的空间的个数,因为递归最深的那一次所耗费的空间足以容纳它所有递归过程。
排序分类:
我们通常所说的排序算法往往指的是内部排序算法,即数据记录在内存中进行排序,不涉及内存、外存之间的数据交换。外部排序算法要进行内存、外存之间的数据交换。
排序算法大体可分为两种:
一种是比较排序,时间复杂度O(nlogn) ~ O(n^2),主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。
另一种是非比较排序,时间复杂度可以达到O(n),主要有:计数排序,基数排序,桶排序等。
下表给出了常见比较排序算法的性能:
冒泡排序
冒泡排序重复地走访过要排序的元素,依次比较相邻两个元素,如果他们的顺序错误就把他们调换过来,直到没有元素再需要交换,排序完成。这个算法的名字由来是因为越小(或越大)的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序的代码如下
public void bubble_sort(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);
}
}
}
冒泡排序的改进:鸡尾酒排序
鸡尾酒排序,也叫定向冒泡排序,是冒泡排序的一种改进。此算法与冒泡排序的不同处在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。
public void CocktailSort(int[] arr) {
int left = 0, right = arr.length - 1;
while (left < right) {
for (int i = left; i < right; i++) {
if (arr[i] > arr[i + 1]) swap(arr, i, i + 1);
}
right--;
for (int i = right; i > left; i--) {
if (arr[i - 1] > arr[i]) swap(arr, i - 1, i);
}
}
}
选择排序(Selection Sort)
选择排序也是一种简单直观的排序算法。它的工作原理很容易理解:初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
(如果每次比较都交换,那么就是交换排序;如果每次比较完一个循环再交换,就是简单选择排序。)
public void selectSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int cur = arr[i];
int position = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < cur) {
cur = arr[j];
position = j;
}
}
arr[position] = arr[i];
arr[i] = cur;
}
}
插入排序(Insertion Sort)
每次将一个待排序的元素,按其关键字大小插入到已经排好序的子表中的适当位置,直到全部元素插入完成为止。
1. 从第一个元素开始,该元素可以认为已经被排序
2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5. 将新元素插入到该位置后
6. 重复步骤2~5
public void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int insesrtNum = arr[i];
int j = i - 1;
for (; j >= 0 && arr[j] > insesrtNum; j--) {
arr[j + 1] = arr[j];
}
arr[j + 1] = insesrtNum;
}
}
二分插入排序,插入排序的改进
public void straightInsertImpl(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int low = 0, high = i - 1, temp = arr[i];
while (low <= high) {
int mid = (low + high) / 2;
if (temp < arr[mid]) high = mid - 1;
else low = mid + 1;
}
for (int j = i - 1; j > high; j--) {
arr[j + 1] = arr[j];
}
arr[high + 1] = temp;
}
}
希尔排序(Shell Sort)
希尔排序,也叫递减增量排序,是插入排序的一种更高效的改进版本。希尔排序是不稳定的排序算法。
希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
9 8 7 6 5 4 3 2 1 0
第一次分为5组: (9,4) (8,3) (7,2) (6,1) (5,0)并将同组内元素进行排序 4 3 2 1 0 9 8 7 6 5 (9和4交换位置,8和3交换位置)
第二次分为2组: (4,2,0,8,6),(3,1,9,7,5) 结果;0 1 2 3 4 5 6 7 8 9
第三次分为1组: 0 1 2 3 4 5 6 7 8 9 (最终结果)
public static void sheelSort(int[] a) { int d = a.length; while (d != 0) { d = d / 2; for (int x = 0; x < d; x++) { for (int i = x + d; i < a.length; i += d) {// 对所有相距gap的元素进行直接插入排序 int j = i - d; int temp = a[i]; for (; j >= 0 && temp < a[j]; j -= d) a[j + d] = a[j]; a[j + d] = temp; } } } }
归并排序(Merge Sort)
归并排序的实现分为递归实现与非递归(迭代)实现,归并操作指的是将两个已经排序的序列合并成一个序列的操作
归并操作步骤如下:
1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
4. 重复步骤3直到某一指针到达序列尾
5. 将另一序列剩下的所有元素直接复制到合并序列尾
public static void mergeSort(int[] numbers, int left, int right) { int t = 1;// 每组元素个数 int size = right - left + 1; while (t < size) { int s = t;// 本次循环每组元素个数 t = 2 * s; int i = left; while (i + (t - 1) < size) { merge(numbers, i, i + (s - 1), i + (t - 1)); i += t; } if (i + (s - 1) < right) merge(numbers, i, i + (s - 1), right); } } private static void merge(int[] data, int p, int q, int r) { int[] B = new int[data.length]; int s = p; int t = q + 1; int k = p; while (s <= q && t <= r) { if (data[s] <= data[t]) { B[k] = data[s]; s++; } else { B[k] = data[t]; t++; } k++; } if (s == q + 1)B[k++] = data[t++]; else B[k++] = data[s++]; for (int i = p; i <= r; i++) data[i] = B[i]; }
堆排序(Heap Sort)
堆排序是指利用堆这种数据结构所设计的一种选择排序算法。堆是一种近似完全二叉树的结构(通常堆是通过一维数组来实现的),并满足性质:以最大堆(也叫大根堆、大顶堆)为例,其中父结点的值总是大于它的孩子节点。
public void heapSort(int[] a) { System.out.println("开始排序"); int arrayLength = a.length; //循环建堆 for (int i = 0; i < arrayLength - 1; i++) { //建堆 buildMaxHeap(a, arrayLength - 1 - i); //交换堆顶和最后一个元素 swap(a, 0, arrayLength - 1 - i); System.out.println(Arrays.toString(a)); } } private void swap(int[] data, int i, int j) { // TODO Auto-generated method stub int tmp = data[i]; data[i] = data[j]; data[j] = tmp; } //对data数组从0到lastIndex建大顶堆 private void buildMaxHeap(int[] data, int lastIndex) { // TODO Auto-generated method stub //从lastIndex处节点(最后一个节点)的父节点开始 for (int i = (lastIndex - 1) / 2; i >= 0; i--) { //k保存正在判断的节点 int k = i; //如果当前k节点的子节点存在 while (k * 2 + 1 <= lastIndex) { //k节点的左子节点的索引 int biggerIndex = 2 * k + 1; //如果biggerIndex小于lastIndex,即biggerIndex+1代表的k节点的右子节点存在 if (biggerIndex < lastIndex) { //若果右子节点的值较大 if (data[biggerIndex] < data[biggerIndex + 1]) { //biggerIndex总是记录较大子节点的索引 biggerIndex++; } } //如果k节点的值小于其较大的子节点的值 if (data[k] < data[biggerIndex]) { //交换他们 swap(data, k, biggerIndex); //将biggerIndex赋予k,开始while循环的下一次循环,重新保证k节点的值大于其左右子节点的值 k = biggerIndex; } else { break; } } } }
快速排序(Quick Sort)
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。
public void quick_sort(int[] arr, int low, int high) {
if (low < high) {
int index = getIndex(arr, low, high);
quick_sort(arr, low, index - 1);
quick_sort(arr, index + 1, high);
}
}
private int getIndex(int[] arr, int low, int high) {
int temp = arr[low];
while (low < high) {
while (low < high && arr[high] >= temp) high--;
arr[low] = arr[high];
while (low < high && arr[low] <= temp) low++;
arr[high] = arr[low];
}
arr[low] = temp;
return low;
}
基数排序
-
将所有的数的个位数取出,按照个位数进行排序,构成一个序列。
-
将新构成的所有的数的十位数取出,按照十位数进行排序,构成一个序列。
public void sort(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++; } } } }
Java系统提供的Arrays.sort函数。对于基础类型,底层使用快速排序。对于非基础类型,底层使用归并排序。请问是为什么?
答:这是考虑到排序算法的稳定性。对于基础类型,相同值是无差别的,排序前后相同值的相对位置并不重要,所以选择更为高效的快速排序,尽管它是不稳定的排序算法;而对于非基础类型,排序前后相等实例的相对位置不宜改变,所以选择稳定的归并排序。
参考:
https://mp.weixin.qq.com/s/QisvlN3u6h-J7sfK5N5AeQ
https://www.cnblogs.com/qlqwjy/p/8696536.html