排序方法
一、基于比较的排序
假设一个有N个数的数组
1、冒泡排序
思想: 冒泡排序是从0位置开始每次把相邻的两个数进行比较,如果0位置的数比较大,则和后一个数继续进行调换,直到调换到末尾,则已经把最大的数放到了最后,则排好了第N-1个位置上的数;接着重复上述的过程,直到排好0位置上的数结束。
实现:
public static void bubbleSort(int arr[])
{
if (arr == null || arr.length<2)
return;
for (int e = arr.length-1; e>0; e--)
{
for (int i=0; i<e; i++)
{
if (arr[i] > arr[i+1])
swap(arr, i, i+1);
}
}
}
public static void swap(int arr[], int a, int b)
{
int temp = arr[a];
arr[a] = arr[b];
arr[b]= temp;
//利用异或^运算来交换两个数的位置,暂时还没弄懂
// arr[a] = arr[a] ^ arr[b];
// arr[b] = arr[a] ^ arr[b];
// arr[a] = arr[a] ^ arr[b];
}
算法复杂度: 冒泡排序的时间复杂度是O(N2)
冒泡排序第一次遍历N次,第二次遍历N-1次,一直到1,求和,就是O(N2)。
2、插入排序
思想: 插入排序就是先排好前n-1个数,然后把第n个数插入到前n-1个数中,就和整理扑克牌一样。
其实就是先排0 ~ 1个数,再排0~2,直到0 ~ n.
实现:
public static void insertionSort(int arr[])
{
if (arr == null || arr.length< 2) //如果数组为空或者只有一个数,则直接返回
return;
for (int i = 1; i<arr.length; i++) //否则开始进行比较
{
for (int j = i-1; j>= 0 && arr[j]>arr[j+1]; j--)
swap(arr, j, j+1);
}
}
public static void swap(int arr[], int a, int b)
{
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
**算法复杂度:**插入排序的算法复杂度为O(N2)
3、选择排序
思想: 每次把最小的数放在第一个,遍历0~N-1,把最小的数放第一个;然后遍历1 ~N-1,把最小的数放在第一位;
实现:
public static void selectionSort(int arr[])
{
if (arr == null || arr.length <2)
return;
for (int i = 0; i < arr.length; i++)
{
int minindex =i;
for (int j= i+1; j<arr.length; j++)
{
minindex = arr[j] < arr[minindex] ? j : minindex;
}
swap(arr, i, minindex);
}
}
public static void swap(int arr[], int a, int b)
{
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
算法复杂度: 选择排序的算法复杂度是O(N2),同样的是第一次遍历N,第二次遍历N-1,类推,所以最后复杂度为O(N2).
冒泡排序和选择排序的区别
冒泡排序和选择排序的区别:
1、冒泡排序,每次是相邻两个数进行比较,把较大的数放在后面;每一次比较如果位置不对都需要调换位置;这个排序是稳定的(不破坏数组本身的顺序);冒泡排序是数找位置(最大的数放最后)。
2、选择排序,每次遍历选择出整个数组最小的数,把最小的数放在最前面;每一轮比较后,最小数和最前面的数换一次位置;这个排序是不稳定的;选择排序是位置找数(最前面的位置放最小的数)。
4、归并排序
思想: 归并排序是先将数组分为左右两部分,然后分别对左边和右边进行排序,最后利用外排(借助一个数组来存储排序的结果)合并两边的排序。
实现:
public static void mergeSort(int arr[])
{
if (arr == null || arr.length <2)
return;
mergeSort(arr, 0, arr.length-1); //调用归并排序
}
public static void mergeSort(int arr[], int l, int r)
{
if (l == r)
return;
int mid = l + ((r -l)>>1); //将数组划分
mergeSort(arr, l, mid); //对左边进行排序
mergeSort(arr, mid+1, r); //对右边进行排序
merge(arr, l, mid, r); //将两边合并起来
}
public static void merge(int arr[], int l, int mid, int r)
{
int help [] = new int[r-l+1]; //设置一个额外的数组来存储排好的顺序
int p1 = l; //左边的指针
int p2 = mid+1; //右边的指针
int i = 0;
while (p1 <= mid && p2 <= r) //比较左右两边的数,哪一边小则哪一边放入到help数组中,同时指针加1,一直比较,直到一方指针到达末尾
{
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
//以下两个while必会执行其中的一个
while (p1 <= mid) //右边的指针先到达末尾
help[i++] = arr[p1++];
while (p2 <= r) //左边的指针先到达末尾
help[i++] = arr[p2++];
for ( i=0; i<help.length; i++) //把排好序的数再放回到arr数组中
arr[l+i]= help[i];
}
算法复杂度: 归并排序的算法复杂度是O(N*logN)
这里利用master公式:
master公式:T(N) = aT(N/b) + O(Nd) (N/b的意思是把样本为N划分为了多大的小样本, a是同样的小样本执行的次数,d是其他常数级别的操作)
logba(log以b为底,a的对数)
1) logba > d, 复杂度O(N的logba 方) (很难打出来,所以用语言描述)
2) logba = d, 复杂度O(Nd*logN)
3) logba < d, 复杂度O(Nd)
在归并排序中,把数组划分为了左右两边,所以b是2,然后对左右两边分别进行了排序,所以a是2,剩下的merge操作是N的一次的操作,所以d是1, 所以归并排序的时间复杂度为O(N*logN)。
额外的空间复杂度为O(N)。(额外的空间复杂度是help数组)
5、随机快排(用得最多的排序方法)
思想:
- 经典快排
将数组的最后一个数作为x,把比x小的数放在数组的左边,把比x大的数放在数组的右边;然后再分别取左边和右边的数的最后一个数作为x,分别执行上述过程。 - 改进快排
将数组的最后一个数作为x,把比x小的数放在数组的左边,和x一样大的放在数组的中间,把比x大的数放在数组的右边;然后再分别取左边和右边的数的最后一个数作为x,分别执行上述过程。 - 随机快排
随机选定数组中的一个数,把它和最后一个数进行调换,然后重复改进快排的过程。 - 三种快排的区别
经典快排每次只能确定一个数的位置,改进快排每次可以确定所有和x相等值的数的位置;经典快排和改进快排每次都是取最后一个数作为x,这样的作法的时间复杂度和数组本身的数据情况好坏有极大的关系,最坏情况下是O(N2),最好情况是O(N* logN)( 这 里 我 还 没 弄 懂 , 做 个 标 记 \color{red}{这里我还没弄懂,做个标记} 这里我还没弄懂,做个标记);随机快排随机选取一个数作为x,这样时间复杂度就是一个长期的期望值,最终就是O(N*logN)。
实现:
随机快排
public static void quickSort(int [] arr)
{
if (arr == null || arr.length < 2) //数组为空或者数组只有一个数,直接返回,不需要排序
return;
quickSort(arr, 0, arr.length-1); //反之进行随机快排
}
public static void quickSort(int [] arr, int l, int r)
{
if (l < r)
{
swap(arr, l+(int)(Math.random()*(r-l+1)), r); //随机选中一个数和数组最后一个数进行交换
int p[] = partition(arr, l, r); //对数组进行划分,比x小的放左边,相等的放中间,大的放右边
quickSort(arr, l, p[0]-1); //对左边进行排序
quickSort(arr, p[1]+1, r); //对右边进行排序
}
}
public static int[] partition(int [] arr, int l, int r)
{
int less = l-1; //小于x的指针的位置,每有一个小于x的数则,less+1位置上的数和小于x的数交换,less+1,l加1
int more = r; //大于x的指针的位置,首先指向最后一个数,每有一个大于x的数,则和more指针的前一个数交换,more+1
while (l < more) //l和more相遇则所有的数都比较完毕
{
if (arr[l] < arr[r])
swap(arr, l++, ++less);
else if (arr[l] > arr[r])
swap(arr, --more, l);
else
l++;
}
swap(arr, r, more); //最后把最后一个数即x和more指向的数进行交换,则more指针后面的数都是比x大的
return new int[]{less + 1, more}; //返回等于x的数组的区间
}
public static void swap(int [] arr, int a, int b)
{
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
算法复杂度:
随机快排的算法复杂度是O(N*logN)。
6、堆排序
首先了解堆的概念,堆,实际上就是一个完全二叉树(完全二叉树包括满二叉树和非满二叉树,但是从左向右是依次补齐的)。
再了解两个概念:大根堆和小根堆
1)大根堆:在完全二叉树中,任何一棵子树的最大值都是这颗子树的头部,所形成的结构叫大根堆。
2)小根堆:在完全二叉树中,任何一棵子树的最小值都是这颗子树的头部。
节点i:
1、左孩子的下标:2i +1;
2、右孩子的下标:2i +2;
3、父节点的下标:(i-1)/2。
思想:
堆排序思想:
1)首先建立一个大根堆;
2)堆顶的数和最后一个数交换,heapsize-1;(heapsize是堆的大小)
3)进行heapfiy操作;(heapfiy是将当前堆调整为大根堆)
4)重复操作2)和3)。
实现:
建立一个大根堆heapInsert(int[] arr, int index)
进来的数和根节点比,然后更新index
将堆调整为大根堆heapfiy(int [] arr, int index, int size):
比较index的两个左右子节点,较大的为largest,然后比较largest和index值的大小,更新largest,如果largest和index一样大,说明不需要调整了,break;否则交换largest和index的值,更新index。
public static void heapSort(int [] arr)
{
if (arr == null || arr.length < 2)
return;
for (int i = 0; i < arr.length; i++)
{
heapInsert(arr, i); //建立大根堆
}
int size = arr.length;
swap(arr, 0, --size); //交换根节点(即最大的值)和最后一个值,堆的大小减1
while (size > 0) //只要堆的大小还大于0,就一直进行上述的操作
{
heapfiy(arr, 0, size); //先把之前的堆再次调整为大根堆
swap(arr, 0, --size); //重复交换根节点和最后一个值,堆的大小减1
}
}
public static void heapInsert(int [] arr, int index) //建立大根堆
{
while (arr[index] > arr[(index-1)/2]) //比较插入的点和其根节点哪个大,把较大的点放到根节点,更新index
{
swap(arr, index, (index-1)/2);
index = (index - 1) / 2;
}
}
public static void heapfiy(int [] arr, int index, int size) //把改变后的堆调整为大根堆
{
int left = index * 2 + 1; //index的左孩子
while (left < size) //左孩子小于size,说明还有右孩子
{
int largest = left + 1 < size && arr[left+1] > arr[left] ? left+1 : left; //比较左右两个孩子的大小
largest = arr[largest] > arr[index] ? largest : index; //比较index和左右孩子中较大的孩子哪个大,更新largest
if (largest == index) //比较largest和index,如果相等,则说明不需要调整堆,
break;
swap(arr, largest, index); //否则交换index和largest
index = largest;
left = index * 2 + 1;
}
}
public static void swap(int [] arr, int a, int b)
{
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
算法复杂度:
堆排序的算法复杂度是O(NlogN),(
不
太
会
分
析
这
里
的
复
杂
度
\color{red}{不太会分析这里的复杂度}
不太会分析这里的复杂度)。
二、非基于比较的排序
1、桶排序
思想:
桶排序就是找出数组中最大的值,然后建立相应的桶,把对应的数字放到对应的桶中,最后再从桶中把数倒出来。
实现:
public static void bucketSort(int [] arr)
{
if (arr == null || arr.length < 2)
return;
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++)
max = Math.max(max, arr[i]);
int[] bucket = new int[max+1]; //max加1是因为还要准备0的桶
for (int i = 0; i < arr.length; i++) //把数组中的数依次放入对应的桶中
{
bucket[arr[i]]++;
}
int i = 0;
for (int j = 0; j < bucket.length; j++) //取出对应桶中的数
{
while(bucket[j]-- > 0)
{
arr[i++] = j;
}
}
}
时间复杂度: