1.排序的概念及应用
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持 不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳 定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
2.常见的排序算法
3.常见排序算法的实现
3.1插入排序
就是把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到 一个新的有序序列 。实际中我们玩扑克牌时,就用了插入排序的思想。
3.1.1 直接插入排序
给定一组数,将其给定一组数,将其分为有序区和无序区;初始状态,有序区为这组数中的第一个数,无序区为剩下数;排序时,将无序区的第一个数与有序区的数进行比较,然后插入到合适的位置;重复上一步骤直到无序区没有数。
举例:
12 ,15 ,9 ,20 ,6 ,31,24 (以下【】内为有序区)
初始状态【12】 ,15 ,9 ,20 ,6 ,31 ,24
第一次排序:【12,15】,9,20,6,31,24(将无序区的15与有序区的12进行比较)
第二次排序:【9,12,15】,20,6,31,24(将无序区的9与有序区的12和15比较)
第三次排序:【9,12,15,20】,6,31,24
第四次排序:【6,9,12,15,20】,31,24
第五次排序:【6,9,12,15,20,31】,24
第六次排序:【6,9,12,15,20,24,31】
代码实现:
java
时间复杂度:O(N^2)
* 最坏情况下:逆序的 5 4 3 2 1
* 最好情况下:本身就是有序的 1 2 3 4 5 O(n)
* 如果数据越有序,直接插入排序越快
* 空间复杂度:O(1)
* 稳定性:稳定的排序
* 本身如果是一个稳定的排序,那么可以实现为不稳定的
* 但是 如果一个排序 本身就是不稳定,能实现为稳定的排序吗?
* @param array
*/
public static void insetSort(int[] array) {
for (int i = 1; i < array.length; i++) {
int tmp = array[i];
int j = i-1;
for (; j >= 0 ; j--) {
if(array[j] > tmp) {
array[j+1] = array[j];
}else {
array[j+1] = tmp;
break;
}
}
array[j+1] = tmp;
}
}
直接插入排序的特性总结:
-
- 元素集合越接近有序,直接插入排序算法的时间效率越高
-
- 时间复杂度:O(N^2)
-
- 空间复杂度:O(1),它是一种稳定的排序算法
-
- 稳定性:稳定
3.1.2 希尔排序
希尔排序法又称缩小增量法,把一组含有n个数的序列,一般取间隔d=n/2(向下取整),按照d将这组数分为d组子序列;然后对着d组子序列依次进行比较,直到这组子序列有序;对d按照初始方法进行分割,然后比较,直到d=1。
举例:
代码实现:
java
/**
* 不稳定的
* 时间复杂度:n^1.3 - n^1.5
* 空间复杂度:O(1)
* @param array
*/
public static void shellSort(int[] array) {
int gap = array.length;
while (gap > 1) {
gap /= 2;//
shell(array,gap);
}
}
private static void shell(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {
int tmp = array[i];
int j = i-gap;
for (; j >= 0 ; j -= gap) {
if(array[j] > tmp) {
array[j+gap] = array[j];
}else {
array[j+gap] = tmp;
break;
}
}
array[j+gap] = tmp;
}
}
希尔排序特性总结:
-
- 希尔排序是对直接插入排序的优化。
-
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很 快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
-
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排 序的时间复杂度都不固定。
- 4.稳定性:不稳定。
3.2 选择排序
基本思想:每一次从待排序的数据元素中选出一个最小(或最大)的元素,存放在序列的起始位置,直到所有待排序元素排完。
3.2.1 直接选择排序
基本思路:将一组数分为有序区和无序区,初始有序区没有数,无序区包含整组数,然后在无序区中挑出最小数与无序区中的第一个数进行交换,并将其加入有序区重复上一步骤直至无序区只剩一个数。
举例:
{49,18,65,97,76,13,38}【】内为有序区
初始【】49,18,65,97,76,13,38
第一次【13】49,18,65,97,76,38
第二次【13,18】49,65,97,76,38
第三次【13,18,38】49,65,97,76
第四次【13,18,38,49】65,97,76
第五次【13,18,38,49,65】97,76
第六次【13,18,38,49,65,76】97
代码实现:
java
/**
* 选择排序:
* 时间复杂度:O(N^2)
* 和数据 是否有序无关
* 空间复杂度:O(1)
* 稳定性:不稳定的排序
* @param array
*/
public static void selectSort2(int[] array) {
for (int i = 0; i < array.length; i++) {
int mindIndex = i;
for (int j = i+1; j < array.length; j++) {
if(array[j] < array[mindIndex]) {
mindIndex = j;
}
}
swap(array,i,mindIndex);
}
}
private static void swap(int[] array,int i,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
进一步优化这个代码
代码实现:
java
private static void swap(int[] array,int i,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
public static void selectSort(int[] array) {
int left = 0;
int right = array.length-1;
while (left < right) {
int minIndex = left;
int maxIndex = left;
for (int i = left+1; i <= right; i++) {
if(array[i] < array[minIndex]) {
minIndex = i;
}
if(array[i] > array[maxIndex]) {
maxIndex = i;
}
}
swap(array,left,minIndex);
//最大值正好是 left下标 此时 把最大值换到了minIndex的位置了
if(maxIndex == left) {
maxIndex = minIndex;
}
swap(array,right,maxIndex);
left++;
right--;
}
}
总结: 1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用 2. 时间复杂度:O(N^2) 3. 空间复杂度:O(1) 4. 稳定性:不稳定。
3.2.2 堆排序
创建大根堆,然后交换堆顶元素和堆中最后一个元素,在进行向下调整。
代码实现:
java
/**
* 堆排序
* 时间复杂度:O(n*logN)
* 空间复杂度:O(1)
* 稳定性:不稳定
* @param array
*/
public static void heapSort(int[] array) {
createHeap(array);
int end = array.length-1;
while (end > 0) {
swap(array,0,end);
siftDown(array,0,end);
end--;
}
}
private static void createHeap(int[] array) {
for (int parent = (array.length-1-1)/2; parent >= 0; parent--) {
siftDown(array,parent,array.length);
}
}
/**
*
* @param array
* @param parent 每棵子树调整的根节点
* @param length 每棵子树调整的结束节点
*/
private static void siftDown(int[] array, int parent, int length) {
int child = 2 * parent + 1;
while (child < length) {
if(child + 1 < length && array[child] < array[child+1]) {
child++;
}
if(array[child] > array[parent]) {
swap(array, parent, child);
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}
总结:1. 堆排序使用堆来选数,效率就高了很多。 2. 时间复杂度:O(N*logN) 3. 空间复杂度:O(1) 4. 稳定性:不稳定 。
3.3 交换排序
基本思想:基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特 点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
3.3.1 冒泡排序
冒泡排序的核心就是多趟排序,若以升序(从小到大)排序为例,假若有N个数。第一趟排序目的是找到整个数组中最大的数并把它排在最后端;最后一个最大数不再比较移动,第二趟排序目的是在剩下的N-1个数找出最大的(即整个数组中第二大的数)并把它放在倒数第二位......这样一轮一轮的比较,直到只剩下一个数时(完成了N趟的排序)这个排序就完成了,从而实现从小到大的排序。
java
/**
* 冒泡排序:
* 时间复杂度:【讨论 没有优化的情况下,也就是 没有下方的boolean元素和-i操作】
* O(N^2)
* 优化以后 可能会达到O(N)
* 空间复杂度:O(1)
* 稳定性:稳定的排序
* @param array
*/
public static void bubbleSort(int[] array) {
for (int i = 0; i < array.length-1; i++) {
boolean flg = false;
for (int j = 0; j < array.length-1-i; j++) {
if(array[j] > array[j+1]) {
swap(array,j,j+1);
flg = true;、//当发生交换时,将flg置为true
}
}
if(!flg) {
break;
}
}
}
总结:1. 冒泡排序是一种非常容易理解的排序 2. 时间复杂度:O(N^2) 3. 空间复杂度:O(1) 4. 稳定性:稳定。
3.3.2 快速排序
基本思想:任取待排序元素序列中的某元 素作为基准值(这里我以第一个数为基准值),按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有 元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
java
public static void quickSort(int[] array) {
quick(array,0,array.length-1);
}
private static void quick(int[] array,int start,int end) {
if(start >= end) {
return;
}
int pivot = partition(array,start,end);//partition为找基准值建立的方法
quick(array,start,pivot-1);
quick(array,pivot+1,end);
}
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,在写递归框架时可想想二叉树前序 遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。 将区间按照基准值划分为左右两半部分的常见方式有:
1.Hoare版
java
private static int partitionHoare(int[] array, int left, int right) {
int tmp = array[left];
int tmpLeft = left;
while (left < right) {
while (left < right && array[right] >= tmp) {
right--;
}
while (left < right && array[left] <= tmp) {
left++;
}
swap(array,left,right);
}
swap(array,left,tmpLeft);
return left;
}
其他问题:1.为什么先从后边找,不先从前边找
2.循环条件中的array[right] >= tmp和array[left] <= tmp必须加等于号。
2.挖坑法
先将第一个数据存放在临时变量K中,形成一个坑位k=,从后找到比这个临时变量小的值,把它放在这个坑位中,自己在变成新坑;再从前面找到比这个储存的临时变量的值大的值,放在上一个数据留下的坑位中,直到left与right相遇,把这个临时变量放在此时的下标中。
java
private static int partition(int[] array, int left, int right) {
int tmp = array[left];
while (left < right) {
while (left < right && array[right] >= tmp) {
right--;
}
array[left] = array[right];
while (left < right && array[left] <= tmp) {
left++;
}
array[right] = array[left];
}
array[left] = tmp;
return left;
}
3.前后指针
然后判断cur指针指向的数据是否小于key,若小于,则prev指针后移一位,看此时cur指向的内容是否与prev指向的内容相同,相同,cur++,不同,交换cur与prev的内容。
java
private static int partition2(int[] array, int left, int right) {
int prev = left ;
int cur = left+1;
while (cur <= right) {
if(array[cur] < array[left] && array[++prev] != array[cur]) {
swap(array,cur,prev);
}
cur++;
}
swap(array,prev,left);
return prev;
}
快速排序的优化
- 三数取中法选key
三数取中是为了选择一个更好的基准值,以提高快速排序的效率。具体来说,三数取中的优化是选择待排序序列的左端、右端和中间位置的三个元素,然后取它们的中值作为基准值。这样选择的基准值相对于最左边或最右边的元素,更接近整个序列的中间位置,可以更好地平衡分割后的两个子序列的长度,从而提高快速排序的效率。
java
private static void quick(int[] array,int start,int end) {
if(start >= end) {
return;
}
int midIndex = getMiddleNum(array,start,end);
swap(array,start,midIndex);
int pivot = partition(array,start,end);
quick(array,start,pivot-1);
quick(array,pivot+1,end);
}
private static int getMiddleNum(int[] array,int left,int right) {
int mid = (left+right)/2;
if(array[left] < array[right]) {
if(array[mid] < array[left]) {
return left;
}else if(array[mid] > array[right]) {
return right;
}else {
return mid;
}
}else {
if(array[mid] > array[left]) {
return left;
}else if(array[mid] < array[right]) {
return right;
}else {
return mid;
}
}
}
- 递归到小的子区间时,可以考虑使用插入排序
我们对递归部分进行一些优化,在满二叉树的最后一层节点会占总结点的50%,对比到快排的递归而言,为了让这10个数有序走了多次递归不划算,那这10个数我们可以采用直接插入的方法减少递归次数增加效率。
代码实现:
java
private static void quick(int[] array,int start,int end) {
if(start >= end) {
return;
}
if(end - start + 1 <= 7) {
insertSortRange(array,start,end);
return;
}
int pivot = partition(array,start,end);
quick(array,start,pivot-1);
quick(array,pivot+1,end);
}
private static void insertSortRange(int[] array,int start,int end) {
for (int i = start+1; i <= end; i++) {
int tmp = array[i];
int j = i-1;
for (; j >= start ; j--) {
if(array[j] > tmp) {
array[j+1] = array[j];
}else {
array[j+1] = tmp;
break;
}
}
array[j+1] = tmp;
}
}
快速排序的非递归实现
这里我们借助栈来实现非递归;
1.我们先找到基准pivot,此时申请一个栈将整个序列的起始和结束位置入栈。然后,进入循环,不断从栈中取出子序列的起始和结束位置。
2.在每次循环中,通过partition函数将当前子序列分割成两部分,并得到基准值的下标pivot。如果基准值右边的子序列长度大于1,则将右边子序列的起始和结束位置入栈。如果基准值左边的子序列长度大于1,则将左边子序列的起始和结束位置入栈。
3.循环继续,直到栈为空,表示所有的子序列都已经排序完成。
代码实现:
java
public static void quickSort(int[] array) {
//quickNor(array,0,array.length-1);
quick(array,0,array.length-1);
}
public static void quickNor(int[] array,int start,int end) {
Deque<Integer> stack = new ArrayDeque<>();
int pivot = partition(array,start,end);
if(pivot > start + 1) {
stack.push(start);
stack.push(pivot-1);
}
if(pivot < end-1) {
stack.push(pivot+1);
stack.push(end);
}
while (!stack.isEmpty()) {
end = stack.pop();
start = stack.pop();
pivot = partition(array,start,end);
if(pivot > start + 1) {
stack.push(start);
stack.push(pivot-1);
}
if(pivot < end-1) { //4:00上课
stack.push(pivot+1);
stack.push(end);
}
}
}
总结:
- 时间复杂度: 最坏情况:当数据给定的是1 2 3 4 5 6 7.....有序的情况下 确实是O(n^2) 9 8 7 6 5 4,最好情况:O(N*logN)
- 空间复杂度: 最坏情况:O(N);最好情况:O(logN)
- 稳定性:不稳定性
3.4 归并排序
基本思想:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使 子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
确定递归的结束条件,求出中间数mid;进行分解,根据mid来确定递归的区间大小;递归分解完左边,然后递归分解右边;左右分解完成后,进行合并;申请新数组进行合并,比较两个数组段,记得查漏补缺;合并的时候要对齐下标,每个tmp的下标要找到array中对应的下标。
java
public static void mergeSort(int[] array) {
mergeSortTmp(array,0,array.length-1);
}
private static void mergeSortTmp(int[] array,int left,int right) {
if(left >= right) {
return;
}
int mid = (left + right) / 2;
mergeSortTmp(array,left,mid);
mergeSortTmp(array,mid+1,right);
//走到这里 全部分解完毕
// 合并
merge(array,left,mid,right);
}
private static void merge(int[] array, int left, int mid, int right) {
int[] tmp = new int[right-left+1];
int k = 0;
int s1 = left;
//int e1 = mid;
int s2 = mid+1;
//int e2 = right;
while (s1 <= mid && s2 <= right) {
if(array[s1] <= array[s2]) {
tmp[k++] = array[s1++];
}else {
tmp[k++] = array[s2++];
}
}
while (s1 <= mid) {
tmp[k++] = array[s1++];
}
while (s2 <= right) {
tmp[k++] = array[s2++];
}
//可以保证tmp数组 是有序的
for (int i = 0; i < k; i++) {
array[i+left] = tmp[i];
}
}
非递归实现归并排序
基本思想:非递归实现与递归实现的思想相似。区别在于,非递归是从单个元素的组开始,逐步扩大为2个元素、4个元素的组(二倍数扩大组数),即序列划分过程和递归是相反的。如此继续,直至完成所有元素的归并。
大致流程
代码实现:
java
public static void mergeSortNor(int[] array) {
int gap = 1;
while (gap < array.length) {
for (int i = 0; i < array.length; i = 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,mid,right);
}
gap *= 2;
}
}
总结:1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。 2. 时间复杂度:O(N*logN) 3. 空间复杂度:O(N) 4. 稳定性:稳定。
4.排序算法复杂度及稳定性分析
5.其他非基于比较排序
1.计数排序
基本思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤: 1. 统计相同元素出现次数 2. 根据统计的结果将序列回收到原来的序列中。
具体实现如下图,先定义一个计数数组,遍历原来的数组array把 每个元素 放到对应的计数数组当中 进行计数,然后一次遍历计数数组。
java
public static void countSort(int[] array) {
//1. 找最大值 和 最小值 来确定 计数数组的大小
int maxVal = array[0];
int minVal = array[0];
for (int i = 1; i < array.length; i++) {
if(array[i] < minVal) {
minVal = array[i];
}
if(array[i] > maxVal) {
maxVal = array[i];
}
}
int len = maxVal - minVal + 1;
int[] count = new int[len];
//2. 遍历原来的数组array把 每个元素 放到对应的计数数组当中 进行计数
for (int i = 0; i < array.length; i++) {
int index = array[i];//count数组的下标值是array数组的值
count[index-minVal]++;
}
//3.依次 遍历计数数组 O(范围)
int index = 0;
for (int i = 0; i < count.length; i++) {
while (count[i] != 0) {
array[index] = i+minVal;
index++;
count[i]--;
}
}
}
特性总结:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。 2. 时间复杂度:O(MAX(N,范围)) 3. 空间复杂度:O(范围) 4. 稳定性:稳定。