排序就是使待排序序列,按照其中的某个或某些关键字的大小(以什么作为比较基准),递增或递减的排列起来的操作。平时如果提到排序,通常指的是排升序(非降序)。通常意义上的排序,都是指的原地排序(in place sort)。
两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,则我们称该算法是具备稳定性的排序算法。如排序如下序列的算法就是一个稳定的排序算法。
排序的应用很广泛,只要涉及比较,选择等都可以使用排序。
待排序序列作为一整个区间,被分为 有序区间 和无序区间,**首先待排序列的第一个元素本身就是有序的,单独作为一个有序区间 ,第一个元素后面的元素作为无序区间,每次选择无序区间的第一个元素,在有序区间内选择合适的位置插入,直到无序区间里面所有的元素都插入到有序区间为止。**大致思路就是这样,一些详细的过程,我在下面画图举个实例来看一下。
假设以数组元素2,1,4,3,6
为例升序排列,首先变量i从1下标指向无序区的第一个元素向前遍历数组,变量j每次从i的后一个位置向后进行遍历数组,把i位置的值放入一个临时变量tem
中,当j位置的值比tem的值大时,把j位置的值向前移动到i位置扩展有序区大小,之后j继续移动寻找tem放入有序区的合适位置,和i位置的值比较,如果j位置的元素还在比i位置的大,继续移动操作,直到j为负数或者找到一个j指向的元素比i指向的小为止。这时就可以把i的值放入有序区的合适位置,这时的关键代码就是 array[j + 1] = tem;
public static void insertSort(int[] array) {
if(array==null) return;
for (int i = 1; i < array.length; i++) {
int j = i - 1;
int tem = array[i];
while (j >= 0) {
if (array[j] > tem) {
array[j+1] = array[j];
j--;
//i--;
} else {
break;
}
}
array[j + 1] = tem;
}
}
时间复杂度:
最好:O(n),数据有序的情况下,只是遍历了一遍数组,最坏:O(n^2),数据逆序的情况下既要遍历数组也要比较数组。空间复杂度O(1),开辟了固定的数组大小。稳定性:稳定。
插入排序的特点:
初始数据越接近有序,时间效率越高。
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序序列中所有记录分成我们选定的这个整数个组,从第一个元素开始分组,两个数据之间的距离为每组元素个数的分在同一个组,并对每一组内的记录进行直接插入排序。然后重复上述分组和排序的工作。最后将所有的元素分在一个组内,对最后一个组再进行最后的排序即可。希尔排序是对直接插入排序的优化,重复的进行分组之后又排序,先使局部有序之后再使整体有序。
当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
由严蔚敏老师的《数据结构》可知希尔排序的时间复杂度和所取得“增量”有关系,时间复杂度是“增量”的一个函数,但目前一个完美的增量序列如何确定还没有决解,由实验及部分数据可知希尔排序的时间复杂度为O(n1.3)-O(n1.5).空间复杂度为O(n).对于增量的取法,书中描述为取一个除了一之外没有公因子的数字作为增量也就是素数。
public static void shell(int[] array,int gap) {
//参数判断
if(array == null) return;
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 {
break;
}
}
array[j+gap] = tmp;
}
}
public static void shellSort(int[] array) {
int gap = array.length;//初始增量为数组长度
while (gap > 1) {
shell(array,gap);
gap = gap/2;//1
}
shell(array,1);
}
由于我们所给的数据个数是不确定的,所以无法找到一个通用的式子计算出我们的增量让它为素数,所以只要遵循合理的将数组分组,每次分组时增量逐渐缩小,最后分为一组即可。上面的代码和直接插入排序的基本一样,只是这里的i=gap,下的内容也相应地换一下。
选择排序,选择排序直接就升序来说,假设第一个元素是最小的,依次和后面的元素比较假设第一个元素比比较的元素大交换两者的位置,不大于不做任何操作,比较下一个元素,当数组元素比较完后,第一趟比较结束,第一个元素为最小值,第二趟比较假设第二个元素是最小的依次和后面的元素比较和交换重复第一步,当比较了n-1趟后元素就是有序的了。
public static void selectSort(int[] array) {
if(array==null||array.length<2){
return;
}
for (int i = 0; i <array.length-1 ; i++) {
int min=i;//默认第一个元素最小
for (int j = i+1; j <array.length; j++){
if(array[min]>array[j]){
min=j;
}
}
if(min!=i){
int tem=array[i];
array[i]=array[min];
array[min]=tem;
}
}
}
时间复杂度**:O(n^2)** 不管有序还是无序的情况,可以适当的优化下,就是在比较的时候如升序,前面的元素比后面的大时不要每次交换,用一个临时变量记录最小值,当一趟遍历完后再交换,但时间复杂度任然是O(n^2) 空间复杂度:o(1), 稳定性:不稳定的排。
基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数,**堆逻辑上是一棵完全二叉树
满足任意结点的值都大于等于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆反之,则是小堆,或者小根堆,或者最小堆,子树中节点的值没有关系。**就升序而言,我们创建一个大堆,每次堆顶的元素都是最大的,我们把它和堆的最后一个元素交换,堆的末尾元素就是最大值,也是数组的最后一个元素就是最大值,有序的元素下次交换调整时排除在排序范围内,之再调整交换堆使它继续为大根堆,之后重复上面的操作,第二次得到第二大元素,直到排序到堆顶元素为止。
注意:
排升序要建大堆;排降序要建小堆。
再简单总结下堆排序的基本思路:
a.将无序序列构建成一个堆,根据升序降序需求选择大根堆或小根堆;
b.将堆顶元素与末尾元素交换,将最大元素或者最小元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个数组有序。
public static void shiftDown(int[] array,int parent,int len) {
int child = 2*parent+1;
while (child < len) {
if(child+1 < len && array[child] < array[child+1]) {
child++;
}
//child下标 表示的就是左右孩子最大值的下标
if(array[child] > array[parent]) {
int tmp = array[child];
array[child] = array[parent];
array[parent] = tmp;
parent = child;//如果子树不为空,操作子树就行,改变的那颗子树
child = 2*parent+1;
}else {
break;
}
}
}
public static void createBigHeap(int[] array) {
for (int i = (array.length-1-1)/2; i >= 0 ; i--) {
shiftDown(array,i,array.length);
}
}
/**
* 时间复杂度:O(N*logn)
* 空间复杂度:O(1)
* 稳定性:不稳定
* @param array
*/
public static void heapSort(int[] array) {
createBigHeap(array);
int end = array.length-1;
while (end > 0) {
int tmp = array[0];
array[0] = array[end];
array[end] = tmp;
shiftDown(array,0,end);
end--;
}
}
时间复杂度:**O(n * log(n)),**可以看成一颗完全二叉树,每个节点都要进行交换,交换之后要进行向下调。整空间复杂度:O(1)
在待排序序列中,通过相邻数的比较,逐渐的将较大的数据移动到待排序序列的后面,持续这个过程,直到数组整体有序。假设有n个数据只需要比较n-1
趟,每一次冒泡出无序序列中的最大值,对于第一趟的比较比较的次数依然是n-1次,之后每趟比较的次数依次减少1。直到n-1趟比较完,数据有序了,下面的代码进行了一些优化,当待排序序列本身有序的时候,完成了一次排序就结束了。
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++) {//每一趟的比较次数都在减少所以要减i
if(array[j] > array[j+1]) {
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
flg = true;
}
}
if(flg == false) {
break;
}
}
}
时间复杂度:O(N^2)数据无序的情况下,O(N)数据有序的情况下,空间复杂度O(1),稳定性:稳定的排序。
public static int partition(int[] array,int start,int end) {
int tmp = array[start];
while (start < end) {
//1、先判断后面
while (start < end && array[end] >= tmp) {//找到一个小于基准点的值
end--;
}
//1.1 后面的给start array[start] = array[end]
array[start] = array[end];
//2、再判断前边
while (start < end && array[start] <= tmp) {
start++;
}
//2.1 把这个大的给end array[end] = array[start]
array[end] = array[start];
}
//start=end
array[start] = tmp;
return start;
}
//类似二叉树搜索树的前序遍历,基准点就是二叉树的根结点
public static void quickSort(int[] array,int left,int right) {
if(left >= right) {//递归结束的条件
return;
}
int pivot = partition(array,left,right);
quickSort(array,left,pivot-1);
quickSort(array,pivot+1,right);
}
public static void main(String[] args) {
int []arr={6,0,1,2,7};
System.out.println(Arrays.toString(arr));
quickSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
假设我以数组5,1,2,4,3,6为例排升序,大致过程如下,将就看下,第一次以5为基准一次划分如下:
接着再以同样的方式,先划分左再划分右一直递归下去,有一个数字的序列不用划分了,默认是有序的。下面以3为基准的划分,以2基准划分,以1基准划分划分完之后,最开始的基准的左边是不是有序了,再开始右边排序,之后整个序列就有序了。
基本思路和挖坑法一致,只是不再进行进行赋值,而是进行两个数的交换,实现了一个交换函数:
public static void quickSort1(int[] array,int left,int right) {
if(left >= right) {//递归结束的条件
return;
}
int pivot = partition1(array,left,right);
quickSort(array,left,pivot-1);
quickSort(array,pivot+1,right);
}
public static int partition1(int[] array, int left, int right) {
int i = left;
int j = right;
int pivot = array[left];
while (i < j) {
while (i < j && array[j] >= pivot) {
j--;
}
while (i < j && array[i] <= pivot) {
i++;
}
swap(array, i, j);
}
swap(array, i, left);
return i;
}
// //交换的方法
public static void swap(int[]array,int i,int j){
int tem=array[i];
array[i]=array[j];
array[j]=tem;
}
时间复杂度:可以把快速排序看成二叉树的前序遍历,每个节点做为基准都遍历了都排好序了,整个数组就有序了,最好情况下O(n * log(n)),每次递归都将待排序序列均匀分割,树的深度和每一层的遍历区间相乘。最坏情况下O(n^2),此时数据有序,就是一个单分支的树了,空间复杂度最坏情况下:O(n)。稳定性:不稳定。
对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列。
基本的快速排序选取第一个或最后一个元素作为基准。但不是一种好方法,**因为如果当数据有序的时候,这样的算法效率很低,每次排序完只能排除一个数字,时间复杂度很大,**就像上面写的快速排序就是固定位置法的快速排序,因此下面介绍效率更高的两种基准选取的方法。
思想:利用随机数,取待排序列中任意一个元素作为基准
public static void quickSort(int[] array,int left,int right) {
if(left >= right) {//递归结束的条件
return;
}
// 1、随机选择基准-》先将left下标的值换一下
//rand 交换array[left] array[rand]
Random random = new Random();
int rand = random.nextInt(right-left)+left+1;
swap(array,left,rand);
int pivot = partition(array,left,right);
quickSort(array,left,pivot-1);
quickSort(array,pivot+1,right);
}
随机选取基准法,较固定位置法效率有所改变,但存在极大的偶然性,不排除你每次选的基准是待排序序列里面的最大值或者最小值,也不能将待排序序列进行均匀的分割。
其实最好的做法就是找到待排序数组的中间值以它进行划分,当这样是很难得到的,一般的做法是使用左端、右端和中心位置上的三个元素的中值作为基准值。显然使用三数中值分割法消除了预排序输入的不好情形。
public static void medianOfThree(int[] array,int left,int right) {
int mid = (left+right)/2;
if(array[mid] > array[left]) {
int tmp = array[mid];
array[mid] = array[left];
array[left] = tmp;
}//array[mid] <= array[start]
if(array[left] > array[right]) {
int tmp = array[left];
array[left] = array[right];
array[right] = tmp;
}//array[start] <= array[right]
if(array[mid] > array[right]) {
int tmp = array[mid];
array[mid] = array[right];
array[right] = tmp;
}
//array[mid] <= array[start] <= array[right]
}
//类似二叉树搜索树的前序遍历,基准点就是二叉树的根结点
public static void quickSort(int[] array,int left,int right) {
if(left >= right) {//递归结束的条件
return;
}
//2、三数取中法
medianOfThree(array,left,right);
int pivot = partition(array,left,right);
quickSort(array,left,pivot-1);
quickSort(array,pivot+1,right);
}
我们知道插入排序的特点是,数据越有序排序速度越快,当待排序序列的长度分割到一定大小后,待排序序列长度在5~20之间任一截止范围都可以使用直接插入排序,而不使用快速排序。
public static void insertSort2(int[] array,int left,int right) {
//参数判断
if(array == null) return;
for (int i = left+1; i <= right; i++) {
int tmp = array[i];
int j = i-1;
for (; j >= left ; j--) {
if(array[j] > tmp) {
array[j+1] = array[j];
}else {
break;
}
}
array[j+1] = tmp;
}
}
//3、递归执行到一个区间之后 进行直接插入排序,加入到导快速排序之中
if((right - left + 1) <= 20) {
insertSort2(array,left,right);
return;//
}
我们通常见到的快速排序为递归版本,并没有关注非递归的方式。但非递归版本也是很好实现的,因为递归的本质是栈
。在非递归实现的过程中,借助栈来保存中间变量就可以实现非递归了。下面的非递归也瞅瞅(* ̄︶ ̄)。
public static void quickSort2(int[] array) {
Stack<Integer> stack = new Stack<>();
int start = 0;
int end = array.length-1;
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.empty()) {
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) {
stack.push(pivot+1);
stack.push(end);
}
}
}
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。==核心就是先分解再合并为有序序列,先把待排序序列分成左右两部分,使这两部分分别有序,再合并这样整体就有序了,对于左边和右边的序列将其进行相同的操作又是一次归并排序,是具有相同结构的子问题。==分解过程我们可以类比二叉树的前序遍历,每一个序列就是一颗二叉树,先根再左后右。先分解成一个一个数字,一个数字是有序的,然后合并为两个数字是有序的,之后两个两个合并四个数字的序列就是有序的了,依次类推再合并,整个序列有序了。
public static void merge(int[] array,int left,int right,int mid) {
//下面就是合并两个有序数组的操作
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 <= e1 && s2 <= e2) {
if(array[s1] <= array[s2]) {
tmp[k++] = array[s1++];
}else {
tmp[k++] = array[s2++];
}
}
while (s1 <= e1) {
tmp[k++] = array[s1++];
}
while (s2 <= e2) {
tmp[k++] = array[s2++];
}
for (int i = 0; i < k; i++) {
//排好序的数字 放到原数组合适的位置
array[i+left] = tmp[i];
}
}
public static void mergeSort1(int[] array,int left,int right) {
if(left >= right) {//递归结束的条件,也就是把待排序序列分成一个一个数字就终止递归
return;
}
int mid = (left+right)/2;
mergeSort1(array,left,mid);//递归左边的序列
mergeSort1(array,mid+1,right);//递归右边的序列
merge(array,left,right,mid);//合并有序序列
}
时间复杂度:O(n * log(n)),二叉树的层数乘上每一层的数据,空间复杂度:O(n),稳定性:稳定。
归并排序的非递归是从小到大算起的和递归的思路相反,核心思路也是合并两个有序数组,两个段两个段进行比较合并为一个有序段,再把前面的有序段合并,首先进行比较的时候一个段里面是一个数,之后分段的个数以二倍递增,之后划分的段数大于等于数组的长度此时,待排序序列也就有序了。
非递归时间复杂度::,O(n * log(n)),空间复杂度:O(n)
//非递归版本
public static void merge2(int[] array,int gap) {
int[] tmp = new int[array.length];
int k = 0;
int s1 = 0;
int e1 = s1+gap-1;//0+1-1
int s2 = e1+1;
int e2 = s2+gap-1 >= array.length ? array.length-1 : s2+gap-1;//e2超出的情况
//一定要有2个段 s2判断 不能拿e2 用s2至少第二个段有1个数据
while (s2 < array.length) {//
while (s1 <= e1 && s2 <= e2) {
if (array[s1] <= array[s2]) {
tmp[k++] = array[s1++];
}else {
tmp[k++] = array[s2++];
}
}
while ( s1 <= e1 ) {
tmp[k++] = array[s1++];
}
while (s2 <= e2) {
tmp[k++] = array[s2++];
}
s1 = e2+1;
e1 = s1+gap-1;
s2 = e1+1;
e2 = s2+gap-1 >= array.length ? array.length-1 : s2+gap-1;
}
//只剩下第一个段了
while (s1 <= array.length-1) {//s1,e1也可能超过数组的范围
tmp[k++] = array[s1++];
}
//一个段都不剩就不管了
for (int i = 0; i < k; i++) {
array[i] = tmp[i];
}
}
public static void mergeSort(int[] array) {
for (int gap = 1; gap < array.length; gap*=2) {//递归是分解,非递归是合并首先一个数一个数一组有序,之后两个一组有序,以此类推
merge2(array,gap);
}
}
非递归版本细节比较的多,相对递归来说比较难写,下面我用图示的方式列举递归需要注意的大致细节,写代码的时候无非就是需要注意一些边界情
况:对于代码中的s1,e1,s2,e2变量分别就是在排序的时候,我们分的两段排序序列第一段的起始位置,结束位置,第二段的起始结束位置(所有的位置都不能超过数组下标)
只有一个段的情况:
传统的排序算法一般指内排序算法,针对的是数据可以一次全部载入内存中的情况。但是面对海量数据,单位可能是GB,TB及以上,而内存只是几十个G而已,即数据不可能一次全部载入内存,这些数据是存储在外部存储器中的,需要用到外排序的方法。由于计算机访问内存的速度比访问外存的速度快还多,但内存又比较贵,外排序采用分块的方法(分而治之),首先将数据分块,对块内数据按选择一种高效的内排序策略进行排序。然后采用归并排序的思想对于所有的块进行排序,得到所有数据的一个有序序列。
例如,考虑一个100G文件,可用内存1G的排序方法。首先将文件分成200份,每份512M并依次载入内存中进行排序,最后结果存入硬盘。得到的是200个分别排序的文件。接着执行200路归并,从每个文件载入一定的数据到输入缓存区,对输入缓存区的数据进行归并排序,输出缓存区写满数据之后数据又写在硬盘上,接着缓存区清空继续写接下来的数据。对于输入缓存区,当读入划分的小的文件里的数据排序完后,载入该文件接下来的数据,一直到所有的200份的所有数据都已经被载入到内存中被处理过。最后我们得到的是一个100G的排序好的存在硬盘上的文件。
**基本思想:对于每一个待排序元素,如果知道了待排序数组中有多少个比它小的元素,那么就可以直接得出排序后该元素应该在什么位置上。**为什么叫非基于比较的排序,就是对于数据的排序,没有直接的进行比较交换,而是通过一些其他的思想实现,具体思路如图示:
public class TestDemo {
public static int [] countSort(int[] array){
//1、统计元素的范围,先找出数组中的最大值与最小值
int minValue = array[0];
int maxValue = array[0];
for(int i = 1 ;i<array.length;i++){
if(array[i]>maxValue){
maxValue = array[i];
}
if(array[i]<minValue){
minValue = array[i];
}
}
//2、开辟计数空间:该范围中最多包含的不同元素种类的个数
int range = maxValue-minValue+1;
//System.out.println(range);
int[] arrayCount = new int[range];
//3、统计每个元素出现的次数
for(int i = 0; i < array.length;i++){
try {
arrayCount[array[i]-minValue]++;//array[i]-minValue元素的存放位置
}catch (Exception e ){
e.printStackTrace();
}
}
int begin=0;
//创建一个新的数组来存储已经排序完成的结果
int []num=new int[array.length];
//4、对元素进行排序
for (int i = 0; i < arrayCount.length; i++) {
if(arrayCount[i]!=0){
for (int j = 0; j <arrayCount[i] ; j++) {
num[begin++]=minValue+i;//怎么放的就怎取元素
}
}
}
return num;
}
public static void main(String[] args) {
int [] array={7,4,9,3};
System.out.println(Arrays.toString(array));
int [] num=countSort(array);
System.out.println(Arrays.toString(num));
}
}
当然非基于比较的排序还有什么基数排序,桶排序等也是非基于比较的排序,也简单,这里我就不做介绍了。
路过的小伙伴记得点赞支持下,感谢!!!