文章目录
—、基本的定义
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
稳定性简单理解:例如1,5,8,6,5,若排序后的结果是1,5,5,6,8,则是稳定的。若是1,5,5,6,8就是不稳定的,原来在前面的排序完成还得在前面,是不能改变的,两个5某种意义上来说并不是相同的。
注意事项:以下描述的logn都是指以2为底的
二、常见的七种基于比较的排序算法
1.直接插入排序(对数据敏感)
private static void insertSort(int[] arr) {
for (int i = 1; i <arr.length ; i++) {
int tmp = arr[i];//插入的牌
int j = i-1;
for (; j >=0 ; j--) {
if(arr[j]>tmp){//插入的牌与手上的牌比大小
arr[j+1]=arr[j];
}else{
break;
}
}
arr[j+1]=tmp;
}
}
这种插入排序就相当于玩扑克牌一样,摸一张牌后要插入到自己手中有序的牌中,保持一个有序的状态。
时间复杂度:最坏情况下(逆序)是O(n^2).最好情况(有序)是O(n)。
(故当数据量趋近于有序,直接插入排序的效率更高)
空间复杂度:O(1)
使用场景:数据量少,趋近于有序时
稳定性:稳定
2、希尔排序(缩小增量排序)
希尔排序是对于直接插入排序的一种优化,是将所有的数据统一分组,例如第一次分组每组5个数据,第二次分组每组2个数据,第三次分组每组一个数据。每一次分组以后都将每组的数据进行直接插入排序,由于数据量少,效率就高,每次分组的组数因越来越少,直到1为止,这样最后一次分组会将所有数据都排序好。这样做的好处就是每一次排序都将数据趋近于有序,直到最后一次,数据已经接近有序,这样排序就会很快。整体而言达到优化的效果。由于取组的可能性无穷,所以时间复杂度并不好计算。时间复杂度参考为O(n^1.3)
到O(n^1.5)
private static void shellSort(int[] arr) {
int gap = arr.length;
while(gap>1){//分组
shell(arr,gap);
gap=gap/2;
}
shell(arr,1);
}
private static void shell(int[] arr, int gap) {//每组的直接插入排序
for (int i =gap ; i <arr.length ; i++) {
int tmp = arr[i];
int j = i-gap;
for (; j >=0 ; j-=gap) {
if(arr[j]>tmp){
arr[j+gap]=arr[j];
}else{
break;
}
}
arr[j+gap]=tmp;
}
}
稳定性:不稳定
3、选择排序
private static void selectSort(int[] arr) {
for (int i = 0; i <arr.length ; i++) {
int count = i;
for ( int j = i+1; j <arr.length ; j++) {
if(arr[j]<arr[count]){
count = j;
}
}
int tmp = arr[i];
arr[i]=arr[count];
arr[count]=tmp;
}
}
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。就是遍历所有的数字把最小的放到第一个位置,然后遍历剩下的数字,把最小的放到第二个位置,以此类推。
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定
使用场景:使用效率不是很好,实际上很少使用
4、堆排序
堆排序分为两步
1.建堆,若是升序则建立大堆,若是降序则建小堆
2.利用堆删除思想进行排序
/**
* 建堆
* @param arr
*/
private static void createHeap(int[] arr) {
for (int i = (arr.length-2)/2; i >=0 ; i--) {
shiftDown(i,arr.length,arr);
}
}
//向下调整
private static void shiftDown(int root,int len,int[]arr) {
int child = root*2+1;
while(child<len){
if(child+1<len&&arr[child]<arr[child+1]){
child++;
}
if(arr[child]>arr[root]){
int tmp = arr[child];
arr[child]=arr[root];
arr[root] = tmp;
root = child;
child=root*2+1;
}else{
break;
}
}
}
//堆排序
private static void heapSort(int[] arr) {
createHeap(arr);
for (int i = 0; i <arr.length-1 ; i++) {
int tmp = arr[0];
arr[0]=arr[arr.length-i-1];
arr[arr.length-i-1]=tmp;
shiftDown(0,arr.length-i-1,arr);
}
}
堆排序使用堆来选数,效率就高了很多。
时间复杂度:O(N*logN)
空间复杂度:O(1)
稳定性:不稳定
5、冒泡排序
冒泡排序是通过交换相邻的两个数字,如果是升序,则是前一个数字大,则交换,反之不换,最大的数字会到最后一位,以此类推,排完所有数据。
1.优化前版本
private static void bubbleSort(int[] arr) {
for (int i = 0; i <arr.length-1 ; i++) {
for (int j = 0; j < arr.length-i-1; j++) {
if(arr[j]>arr[j+1]){
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1]=tmp;
}
}
}
}
冒泡排序是一种非常容易理解的排序
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
冒泡排序还有优化的版本,即如果已经排序完成就不用继续再交换了。故这样对有序的数据的排序十分的快。
优化后版本
private static void bubbleSort2(int[] arr) {
for (int i = 0; i <arr.length-1 ; i++) {
boolean flag = false;
for (int j = 0; j < arr.length-i-1; j++) {
if(arr[j]>arr[j+1]){
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1]=tmp;
flag = true;
}
}
if(!flag){
break;
}
}
}
6.快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后对左右子序列重复该过程,直到所有元素都排列在相应位置上为止。其思想与二叉树类似。
1.Hoare法
/**
* 快速排序
* @param arr 传进来的数组
* @param low 起始位置
* @param high 终止位置
*/
private static void quickSort(int[] arr,int low,int high) {
if(low>=high){ //递归结束条件
return;
}
int pivot = partition(arr,low,high);//获取基准值坐标
quickSort(arr,low,pivot-1);//递归基准值左边的数据
quickSort(arr,pivot+1,high);//递归基准值右边的数据
}
private static int partition(int[] arr, int low, int high) {
int tmp = arr[low];
int i = low;
while(low<high) {
while (low < high && arr[high] >= tmp) {
high--;
}
while (low < high && arr[low] <= tmp) {
low++;
}
int tmp1 = arr[low];
arr[low]=arr[high];
arr[high]=tmp1;
}
int tmp1 = arr[i];
arr[i]=arr[high];
arr[high]=tmp1;
return high;
}
上面是 Hoare版的快排,基准值是采取最左端为基准值,下面类似
时间复杂度:好的情况O(n*logn),坏的情况下O(n^2)
空间复杂度:好的情况下:O(logn),坏的情况下O(n)
稳定性:不稳定
2.挖坑法
private static int partition2(int[] arr, int low, int high) {
int tmp = arr[low];
while(low<high){
while(low<high&&arr[high]>=tmp){
high--;
}
arr[low]=arr[high];
while(low<high&&arr[low]<=tmp){
low++;
}
arr[high]=arr[low];
}
arr[high]=tmp;
return high;
}
以上是挖坑法的快排,与Hoare版的区别就是获得基准值坐标的方法不同。
3.前后指针法
//前后指针法
private static int partition3(int[] arr, int low, int high) {
int tmp = arr[low];
int left = low+1;
for (int i = low+1; i <=high ; i++) {
if(arr[i]<tmp){
int tmp1= arr[i];
arr[i]=arr[left];
arr[left]=tmp1;
left++;
}
}
int tmp1= arr[low];
arr[low]=arr[left-1];
arr[left-1]=tmp1;
return left-1;
}
以上是前后指针法,与前两种类似,只是求基准坐标的方法不同。
优化方法
1.将以上三种方法和直接插入排序混用,当递归的数据量小于一个值时(high-low+1<number),进行直接插入排序,会减少很多一部分递归所需要的时间与空间,这里的直接插入排序是有边界的,不是整个数据,是从low到high的数据.
2.快速排序是一种分治的思想,要想算法效率高,就得使得选出的基准值是尽可能可以均匀分数据的。
三数取中法
就是在三个数据中选出大小在中间的数据,尽量可能满足选出的基准值是尽可能可以均匀分数据的。减少递归的深度
private static void quickSort(int[] arr,int low,int high) {
if(low>=high){
return;
}
int mid = getmid(arr,low,high);
int tmp = arr[low];
arr[low]=arr[mid];
arr[mid] = tmp;
int pivot = partition3(arr,low,high);
quickSort(arr,low,pivot-1);
quickSort(arr,pivot+1,high);
}
//获取中间位置的坐标
private static int getmid(int[] arr, int low, int high) {
int mid = low+(high-low)/2;
if(arr[low]<arr[high]){
if(arr[low]>arr[mid]){
return low;
}else if(arr[high]<arr[mid]){
return high;
}else{
return mid;
}
}else{
if(arr[low]<arr[mid]){
return low;
}else if(arr[high]>arr[mid]){
return high;
}else{
return mid;
}
}
}
非递归快速排序
//非递归的快速排序
private static void quickSort2(int[] arr, int left, int right) {
int pivot = partition3(arr,left,right);
Stack<Integer>stack = new Stack<>();
if(left+1<pivot){
stack.push(left);
stack.push(pivot-1);
}
if(pivot+1<right){
stack.push(pivot+1);
stack.push(right);
}
while(!stack.empty()){
right=stack.pop();
left = stack.pop();
pivot = partition3(arr,left,right);
if(left+1<pivot){
stack.push(left);
stack.push(pivot-1);
}
if(pivot+1<right){
stack.push(pivot+1);
stack.push(right);
}
}
}
7.归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
//归并排序
private static void mergeSort(int[] arr, int low, int high) {
if(low>=high){
return;
}
int mid = (low+high)/2;
mergeSort(arr,low,mid);
mergeSort(arr,mid+1,high);
merge(arr,low,mid,high);//给每一部分的数据进行合并
}
private static void merge(int[] arr, int low, int mid, int high) {
int[]tmp = new int[high-low+1];
int a = low;
int a1 = mid;
int b = mid+1;
int b1 = high;
int i = 0;
while(a<=mid&&b<=b1){ //把小的给到中间数组,使得中间数组是有序的
if(arr[a]<arr[b]){
tmp[i++] = arr[a++];
}else{
tmp[i++] = arr[b++];
}
}
while(a<=mid){
tmp[i++] = arr[a++];
}
while(b<=b1){
tmp[i++] = arr[b++];
}
//把中间数组重新赋值给原数组
for (int j = 0; j <tmp.length ; j++) {
arr[j+low]=tmp[j];
}
}
时间复杂度: O(logn*n)(和数据是否有序无关)
空间复杂度:O(n)
稳定性:稳定
非递归归并排序
//非递归归并排序
private static void mergeSortNor(int[] arr, int low, int high) {
int gap = 1;
while(gap<arr.length){
for (int i = 0; i <arr.length ; i+=2*gap) {
int left = i;
int mid =left +gap-1;
if(mid>=arr.length){
mid=arr.length-1;
}
int right = mid +gap;
if(right>=arr.length){
right=arr.length-1;
}
merge(arr,left,mid,right);
}
gap*=2;
}
}
private static void merge(int[] arr, int low, int mid, int high) {
int[]tmp = new int[high-low+1];
int a = low;
int a1 = mid;
int b = mid+1;
int b1 = high;
int i = 0;
while(a<=mid&&b<=b1){ //把小的给到中间数组,使得中间数组是有序的
if(arr[a]<arr[b]){
tmp[i++] = arr[a++];
}else{
tmp[i++] = arr[b++];
}
}
while(a<=mid){
tmp[i++] = arr[a++];
}
while(b<=b1){
tmp[i++] = arr[b++];
}
//把中间数组重新赋值给原数组
for (int j = 0; j <tmp.length ; j++) {
arr[j+low]=tmp[j];
}
}
非基于比较排序
1.计数排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
//计数排序
private static void countSort(int[] arr) {
//获取最大值和最小值
int min = arr[0];
int max = arr[0];
for (int i = 1; i <arr.length ; i++) {
if(arr[i]<min){
min = arr[i];
}
if(arr[i]>max){
max = arr[i];
}
}
//开始计数
int []count = new int[max-min+1];
for (int i = 0; i <arr.length ; i++) {
count[arr[i]-min]++;
}
int index = 0;
for (int i = 0; i <count.length ; i++) {
while(count[i]>0){
arr[index] = min+i;
count[i]--;
index++;
}
}
}
时间复杂度:O(MAX(N,范围)) 范围是指count数组的大小
空间复杂度:O(范围)
基数排序与桶排序不作详细介绍。