- 排序的概念以及引用
1.1 排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i] = r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法时稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。(所有数据所占空间不是很大,能够全部放到内存中)
外部排序:数据元素过多不能同时放在内存中,排序过程的要求不能在内外存之间移动数据的顺序。
1.2 排序的运用
如:购物网站上,对商品进行价格从低到高排序;
某省考试学生全省排名;
世界500强公司;
世界富豪榜排名。
1.3 常见的排序算法
2.常见的排序算法的实现
2.1 插入排序
2.1.1 基本思想
直接插入排序时一种简单的插入排序,其基本思想时:
把待排序的记录按照其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插完为止,得到一个新的有序序列。实际中我们玩扑克牌时,就使用到了插入排序的思想。
2.1.2 直接插入排序
当插入第i(i>=1)个元素时,前面的array[0],array[1],....,array[i-1]已经是有序的了,此时我们用array[i]的排码与array[i-1],array[i-2],...的排码进行比较,找到插入的位置即将array[i]进行插入,原来位置上元素向后移动。
直接插入排序的特征总结:
元素集合越接近有序,直接插入排序算法的时间效率越高。
时间复杂度为O(N^2)
空间复杂度为O(1),它时一种稳定的排序算法。
稳定性:稳定的
【代码实现】
public static void insertSort(int[] array) {
if(array == null) {
return;
}
int length = array.length;
if(length <= 1) {
return;
}
for (int i = 1; i < length; i++) {
int tmp = array[i];
int j;
for (j = i - 1; j >= 0; j--) {
if(tmp < array[j]) {
array[j + 1] = array[j];
}else {
break;
}
}
array[j + 1] = tmp;
}
}
public static void main(String[] args) {
int[] array = new int[]{10,20,44,65,17,13,12};
insertSort(array);
System.out.println(Arrays.toString(array));
}
【注意】我们要是这个集合具有稳定性,我们要认真的思考什么时候才插入tmp
2.1.3 希尔排序(缩小增量排序)
希尔排序又称缩小增量排序。希尔排序法的基本能思想时:先选定一个整数,把待排序文件中所有记录分成多个组,组数为gap,所有距离为总数/gap的记录分为同一组内,并对每一组内的记录进行排序。然后缩小gap的值,重复上述的操作,直到gap = 1时,所有记录在同一组内排好序。
希尔排序的特性总结:
希尔排序是对直接插入的优化。
我们直到直接排序的时间复杂度是O(N^2),如果每个集合元素个数少,那么时间复杂度是很低的。
当gap > 1时都是预排序,目的是让数组更接近于有序排序。当gap == 1时,数组已经接近有序排序了,这样就会很快,对于整体而言,可以达到优化的效果,我们实现后可以进行性能测试(从系统中获取程序开始的时间和程序结束的时间,从差值我们就能判断程序的快慢)进行对比。
希尔排序的时间复杂度不好计算,因为gap的取值方法是多样的,导致很难去计算,因此在很多书中给出的希尔排序的时间复杂度是不固定的,一般为O(N^1.25)到(1.6*N^1.25)
稳定性:不稳定
//希尔排序
public static void shellSort(int[] array) {
if(array == null) {
return;
}
int length = array.length;
if(length <= 1) {
return;
}
int gap = length / 2;
while(gap >= 1) {
for (int i = gap; i < length; i++) {
int tmp = array[i];
int j;
for (j = i - gap; j >= 0; j = j - gap) {
if(tmp < array[j]) {
array[j + gap] = array[j];
}else {
break;
}
}
array[j + gap] = tmp;
}
gap = gap / 2;
}
}
//产生随机数,生成数组,用于测试
public static void initArray(int[] array) {
Random random = new Random();
for (int i = 0; i < array.length; i++) {
array[i] = random.nextInt(100000);
}
}
public static void main(String[] args) {
int[] array = new int[100000];
initArray(array);
int[] array1 = Arrays.copyOf(array, array.length);
long stack1 = System.currentTimeMillis();
insertSort(array1);
long end1 = System.currentTimeMillis();
//System.out.println(Arrays.toString(array1));
System.out.println(end1 - stack1);
int[] array2 = Arrays.copyOf(array, array.length);
long stack2 = System.currentTimeMillis();
shellSort(array2);
long end2 = System.currentTimeMillis();
//System.out.println(Arrays.toString(array2));
System.out.println(end2 - stack2);
}
我们发现直接插入排序的所用时间很慢(单位:毫秒),而使用希尔排序所用时间是很快的。
2.2选择排序
2.2.1基本思想
每一次从待排序的元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
2.2.2直接选择排序
在元素集合array[i]--array[n-1]中选择关键码最大(或最小)的数据元素。
若它不是这组元素中的最后一个(或第一个)元素,则将它与这组元素中的最后一个元素(第一个)元素交换;
在剩余的array[i]--array[n-2] (array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩下一个元素。
【直接选择排序的特性总结】
直接选择排序思考非常容易理解,但是效率不高,实际中使用很少。
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
【代码实现】
//选择排序
public static void selectSort(int[] array) {
if(array == null) {
return;
}
int length = array.length;
if(length <= 1) {
return;
}
//从小到大排序
for (int i = 0; i < length; i++) {
int minIndex = i;
for (int j = i+1; j < length; j++) {
if(array[minIndex] > array[j]) {
minIndex = j;
}
}
int tmp = array[i];
array[i] = array[minIndex];
array[minIndex] = tmp;
}
}
另一种方法,遍历的时候我们同时找到最大值和最小值,同时给最大值和最小值进行排序。
【代码实现】
//选择排序,同时给最大值和最小值排序
public static void selectSort1(int[] array) {
if(array == null) {
return;
}
int left = 0;
int right = array.length - 1;
int min = 0;
int max = right - 1;
if(right < 1) {
return;
}
//从小到大排序
while(left < right) {
for (int i = left; i <= right; i++) {
if(array[min] > array[i]) {
min = i;
}
if(array[max] < array[i]) {
max = i;
}
}
//最小值,进行交换
int tmp = array[left];
array[left] = array[min];
array[min] = tmp;
//最大值进行交换
//判断最大值是否是left下标
if(left == max) {
max = min;
}
tmp = array[right];
array[right] = array[max];
array[max] = tmp;
left++;
right--;
}
}
2.2.3堆排序
堆排序(Heapsort)是指利用堆这种数据结构所涉及的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序要建小堆。
【解析】
例如我们排升序:
我们先讲所有数据以大根堆的形式排列;
我们讲根节点与最后的叶子节点进行交换,然后进行向下调整;
数据的有效个数减一(下次不用在排);
如此循环,最后就是从小到大排序。
【代码】
//堆排序,从小到达排序。
public static void HeapSort(int[] array) {
HeapSortFunc(array);
int i = array.length - 1;
while(i > 0) {
int tmp = array[0];
array[0] = array[i];
array[i] = tmp;
shiftDown(array, 0, i - 1);
i--;
}
}
//建立大根堆
private static void HeapSortFunc(int[] array) {
if(array.length < 2) {
return;
}
int max;
int length = array.length;
int i = (length - 1 - 1) / 2;
while(i >= 0) {
shiftDown(array, i, length - 1);
i--;
}
}
//向下调整
private static void shiftDown(int[] array, int s, int e){
while(true) {
int max;
if(s * 2 + 1 <= e) {
max = s * 2 + 1;
if(s * 2 + 2 <= e && array[s*2+1] < array[s*2+2]) {
max++;
}
if(array[s] < array[max]) {
int tmp = array[s];
array[s] = array[max];
array[max] = tmp;
}else {
break;
}
s = max;
}else {
break;
}
}
}
【堆排序的特征】
时间复杂度是O(N*logN);
空间复杂度:O(1);
稳定性:不稳定;
2.3交换排序
基本思想:所谓交换,就是根据序列中两个记录关键值比较结果来对这两个记录在序列中的位置,交换排序的特点是:讲关键值较大的记录向序列的尾部移动,较小的记录向序列的前部移动。
2.3.1冒泡排序
不做介绍,直接上代码
【代码】
//冒泡排序
public static void bubbleSort(int[] array) {
int end = array.length - 1;
boolean key = true;
while(end > 0) {
key = true;
for (int i = 0; i < end; i++) {
if(array[i] > array[i + 1]) {
int tmp = array[i];
array[i] = array[i+1];
array[i + 1] = tmp;
key = false;
}
}
end--;
if(key) {
return;
}
}
}
【冒泡排序特性总结】
时间复杂度是:O(N^2);
空间复杂度是O(1);
稳定性:稳定的
2.3.2快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序讲待排序集合分割成两子序列,左子序列中所有元素均小于基准值,走子序列中所有元素均大于基准值,然后最后子序列重复该过程,直到所有元素排列到相应的位置上。
1.挖坑法
先让第一个数据存放在临时变量key中,形成一个坑位;
设置left和right,让left从第二个数据开始,right从最后一个元素开始,先让left找到大于key的值,让right位置的元素放在第一个位置上;然后left找到大于key的元素,让left位置的元素放在原来right的位置上。重复此过程直到left>=right
这样我们就得到两个数组,再重复上述方法,直到数据长度等于1
//快速排序
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
private static void quick(int[] array, int start, int end) {
//为什么取大于号,如:1,2,3,4,5,6
if(start >= end) {
return;
}
//为了优化,我们在剩余元素不多的情况下,我们可以直接插入排序(这是数组基本有序,复杂度不会很大)
if(end - start + 1 <= 14) {
//直接插入排序
insertSort(array, start, end);
return;
}
int key = partition(array, start, end);//划分的关键元素的下标
quick(array, start, key - 1);
quick(array, key + 1, end);
}
//挖坑法
private static int partition(int[] array, int start, int end) {
int tmp = array[start];
while(start < end) {
//找小的元素,调换到前面
while(start < end && array[end] >= tmp) {
end--;
}
array[start] = array[end];
//找到大的元素调换到后面
while(start < end && array[start] <= tmp) {
start++;
}
array[end] = array[start];
}
array[start] = tmp;
return start;
}
//直接插入排序
public static void insertSort(int[] array, int start, int end) {
if(start >= end) {
return;
}
for (int i = start + 1; i <= end; i++) {
int tmp = array[i];
int j;
for (j = i - 1; j >= start; j--) {
if(tmp < array[j]) {
array[j + 1] = array[j];
}else {
break;
}
}
array[j + 1] = tmp;
}
}
2.Hoare版
我们讲第一个元素的下标定为key;
设置start为0,end为length-1;
如果array[start]<=array[key],start++;如果array[end]>=array[key],end--;交换两个元素,直到start>=right;最后交换array[left]和第一个元素的下标。
此时根据start的下标,我们可以讲数组分为两组,重复上述过程直到数组元素个数小于2。
//快速排序
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
private static void quick(int[] array, int start, int end) {
//为什么取大于号,如:1,2,3,4,5,6
if(start >= end) {
return;
}
//为了优化,我们在剩余元素不多的情况下,我们可以直接插入排序(这是数组基本有序,复杂度不会很大)
if(end - start + 1 <= 14) {
//直接插入排序
insertSort(array, start, end);
return;
}
//int key = partition1(array, start, end);//划分的关键元素的下标
int key = partition2(array, start, end);
quick(array, start, key - 1);
quick(array, key + 1, end);
}
//Hoare法
private static int partition2(int[] array, int start, int end) {
int key = array[start];
int left = start;
int right = end;
while(left < right) {
while(left < right && array[right] >= key) {
right--;
}
while(left < right && array[left] <= key) {
left++;
}
int tmp = array[left];
array[left] = array[right];
array[right] = tmp;
}
array[start] = array[left];
array[left] = key;
return left;
}
【思考题】 为什么我们先从右边开始找小于key的元素?我们最先从左边开始找大于key的元素可以吗?
不可以,如果先从左边开始找,left开始就在大于key的位置;如果此时right与left相遇,就会导致大于key的元素放在了队首位置,key跑到了中间,最后无法有效排序,如下面的例子:
3.前后指针法
//快速排序
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
private static void quick(int[] array, int start, int end) {
//为什么取大于号,如:1,2,3,4,5,6
if(start >= end) {
return;
}
//为了优化,我们在剩余元素不多的情况下,我们可以直接插入排序(这是数组基本有序,复杂度不会很大)
if(end - start + 1 <= 14) {
//直接插入排序
insertSort(array, start, end);
return;
}
//int key = partition1(array, start, end);//划分的关键元素的下标
//int key = partition2(array, start, end);
int key = partition3(array, start, end);
quick(array, start, key - 1);
quick(array, key + 1, end);
}
//前后指针法
private static int partition3(int[] array, int left, int right) {
int prev = left;
int cur = left + 1;
while(cur <= right) {
while(array[cur] < array[left] && ++prev != cur) {
int tmp = array[prev];
array[prev] = array[cur];
array[cur] = tmp;
}
cur++;
}
int tmp = array[left];
array[left] = array[prev];
array[prev] = tmp;
return prev;
}
以上所介绍的所有快速排序的方法的时间复杂度:
最好情况下:O(N*logN)
最坏情况下:O(N^2)
空间复杂度:O(1)
那么我们如何避免最坏情况的出现呢?
2.3.2快速排序的优化
三数取中法选key;
我们选取start和right和两者中间值,进行大小比较,选择中间的值与首元素进行交换。
//快速排序
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
private static void quick(int[] array, int start, int end) {
//为什么取大于号,如:1,2,3,4,5,6
if(start >= end) {
return;
}
//为了优化,我们在剩余元素不多的情况下,我们可以直接插入排序(这是数组基本有序,复杂度不会很大)
if(end - start + 1 <= 14) {
//直接插入排序
insertSort(array, start, end);
return;
}
//为了优化采取三数取中法
midThree(array, start, end);
//int key = partition1(array, start, end);//划分的关键元素的下标
//int key = partition2(array, start, end);
int key = partition3(array, start, end);
quick(array, start, key - 1);
quick(array, key + 1, end);
}
//为了优化快速排序,我们采取三数取中法
private static void midThree(int[] array, int left, int right) {
int key;
int mid = (left + right) / 2;
if(array[left] < array[right]) {
if(array[mid] < array[left]) {
key = left;
}else if(array[right] < array[mid]) {
key = right;
}else {
key = mid;
}
}else {
if(array[left] < array[mid]) {
key = left;
}else if(array[right] > array[mid]) {
key = right;
}else {
key = mid;
}
}
int tmp = array[left];
array[left] = array[key];
array[key] = tmp;
}
归并到小的子区间中可以考虑直接插入法(如上面代码中,如果数组的有效元素个数小于14时,我们直接插入法)
2.3.3快速排序法的非递归方案
//快速排序的非递归方法
public static void quickSortNonR(int[] array, int left, int right) {
Stack<Integer> s = new Stack<>();
s.push(left);
s.push(right);
while(!s.isEmpty()) {
right = s.pop();
left = s.pop();
if(right - left <= 14) {
insertSort(array, left, right);
continue;
}
midThree(array, left, right);
int key = partition1(array, left, right);
s.push(left);
s.push(key - 1);
s.push(key + 1);
s.push(right);
}
}
2.4归并排序
2.4.1基本思想
归并排序(MERGE—SORT)是建立在归并操作的以一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常经典的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列有序。若将两个有序表合成一个有序表,称为二路归并。归并排序的核心步骤如下:
//归并排序
public static void mergeSort(int[] array) {
mergeSortFunc(array, 0, array.length - 1);
}
private static void mergeSortFunc(int[] array, int left, int right) {
if(left >= right) {
return;
}
int mid = (left + right) / 2;
mergeSortFunc(array, left, mid);
mergeSortFunc(array, mid + 1, right);
merge(array, left, right, mid);
}
private static void merge(int[] array, int start, int end, int mid) {
int s1 = start;
int s2 = mid + 1;
int[] ret = new int[end - start + 1];
int i = 0;
while(s1 <= mid && s2 <= end) {
if(array[s1] < array[s2]) {
ret[i++] = array[s1++];
}else {
ret[i++] = array[s2++];
}
}
while(s1 <= mid) {
ret[i++] = array[s1++];
}
while(s2 <= end) {
ret[i++] = array[s2++];
}
for (int j = 0; j < ret.length; j++) {
array[j+start] = ret[j];
}
}
//归并排序的非递归方法。
public static void mergeSort1(int[] array) {
int gap = 1;
while(gap < array.length) {
for (int i = 0; i < array.length; i += gap * 2) {
int left = i;
int mid = left + gap - 1;
if(mid >= array.length) {
mid = array.length - 1;
}
int right = mid + gap;
if (right >= array.length) {
right = array.length - 1;
}
merge(array, left, right, mid);
}
gap *= 2;
}
}
2.4.2归并排序总结
归并排序其确定在于需要O(N)的空间复杂度,归并排序的思考更多的是为了解决在磁盘中的外排序问题。
时间复杂度:O(N*longN);
空间复杂度:O(N);
稳定性:稳定
2.4.3海量数据的排序问题
外部排序:排序过程需要在磁盘等外部储存进行的排序;
前提:内存的空间小于所需排序数据的空间大小;
因为内存无法将所有的数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序。
先把文件切分成200份,每个512M;
分别对512M排序,因为内存已经可以放的下,所以我们可以任意选择排序方式;
进行2路归并,同时对200份有序文件做归并过程,最终结果就有序了。
3.排序算法复杂度及稳定性分析
排序方法 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
冒泡排序 | O(n^2) | O(n^2) | O(1) | 稳定 |
插入排序 | O(n^2) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(1) | 不稳定 |
希尔排序 | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(n*logn) | O(n*logn) | O(1) | 不稳定 |
快速排序 | O(n*logn) | O(n^2) | O(logn)~O(n) | 不稳定 |
归并排序 | O(n*logn) | O(n*logn) | O(n) | 稳定 |
4.其他非基于比较排序(了解)
计数排序(代码省略)
思想:计数排序又称为鸽巢排序,是对哈希直接定址法的变形应用。基本操作:
统计相同元素出现的次数;
根据统计的结果将序列回收到原来的序列中
【例题】我们有范围1-100的数字,从小到达请进行排序。
【解题思路】创建一个数组array[100],我们从头遍历数组,元素为5,我们让array[5]++,最后遍历我们创建的数组,在原数组的基础上进行修改即可。
【计数排序的特性总结】
计数排序在数据范围集中时,效率很高,但是使用范围即场景有限;
时间复杂度:O(max(N,数据大小范围))
空间复杂度:O(范围)
稳定性:稳定的。
2.基数排序
【解析】
我们将这组数据,先按照个位的数字放在相应的“桶中”,再依次拿出来;
再按照十位的数字放到相应的“桶中”,再依次拿出来;
直到到达最大数字的最高位,再一次拿出来,就是所需结果。
【代码】
//基数排序,这里我们只考虑正数的情况。
public static void radixSort(int[] array) {
int max = array[0];
for (int i = 0; i < array.length; i++) {
if(max < array[i]) {
max = array[i];
}
}
int maxPlace = 1;
while(!(max <=9 && max >= 0)) {
max /= 10;
maxPlace++;
}
Queue<Integer> q0 = new LinkedList<>();
Queue<Integer> q1 = new LinkedList<>();
Queue<Integer> q2 = new LinkedList<>();
Queue<Integer> q3 = new LinkedList<>();
Queue<Integer> q4 = new LinkedList<>();
Queue<Integer> q5 = new LinkedList<>();
Queue<Integer> q6 = new LinkedList<>();
Queue<Integer> q7 = new LinkedList<>();
Queue<Integer> q8 = new LinkedList<>();
Queue<Integer> q9 = new LinkedList<>();
int place = 1;
while(place <= maxPlace) {
for (int i = 0; i < array.length; i++) {
int key = array[i] % (int)(Math.pow(10, place));
int tmpPlace = place;
while(--tmpPlace > 0) {
key = key / 10;
}
switch (key) {
case 0:
q0.offer(array[i]);
break;
case 1:
q1.offer(array[i]);
break;
case 2:
q2.offer(array[i]);
break;
case 3:
q3.offer(array[i]);
break;
case 4:
q4.offer(array[i]);
break;
case 5:
q5.offer(array[i]);
break;
case 6:
q6.offer(array[i]);
break;
case 7:
q7.offer(array[i]);
break;
case 8:
q8.offer(array[i]);
break;
case 9:
q9.offer(array[i]);
break;
}
}
int i = 0;
while(!q0.isEmpty()) {
array[i++] = q0.poll();
}
while(!q1.isEmpty()) {
array[i++] = q1.poll();
}
while(!q2.isEmpty()) {
array[i++] = q2.poll();
}
while(!q3.isEmpty()) {
array[i++] = q3.poll();
}
while(!q4.isEmpty()) {
array[i++] = q4.poll();
}
while(!q5.isEmpty()) {
array[i++] = q5.poll();
}
while(!q6.isEmpty()) {
array[i++] = q6.poll();
}
while(!q7.isEmpty()) {
array[i++] = q7.poll();
}
while(!q8.isEmpty()) {
array[i++] = q8.poll();
}
while(!q9.isEmpty()) {
array[i++] = q9.poll();
}
place++;
}
}
3.桶排序
桶排序:将所有数据按照范围进行分组(1-10,10-20,20-30,30-40,40-50),再进行排序。
5.练习题
快速排序算法是基于(分治法)的一个排序算法。
对记录(54,38,96,23,15,72,60,45,83)进行从小到大的直接俄插入排序时,当把第8个数据记录45插入到有序表时,为找到插入的位置需比较(5)次
以下排序方法中占用O(n)辅助储存空间的是(归并排序)
下列排序方式中稳定且时间复杂度为O(n^2)的是(冒泡排序)
关于排序,下面说法不正确的是(归并排序空间复杂度是O(n),堆排序的空间复杂度是O(logn))
设一组初始记录关键字序列为(65,56,72,99,86,25,34,66),则以第一个关键字65排序为基准而得到的快速排序结果是(34,56,25,65,86,99,72,66)