各类排序算法
比较排序
时间复杂度O(nlogn) ~ O(n^2)
主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。
冒泡排序
实现:比较相邻的元素。如果第一个比第二个大,就交换它们两个;
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
针对所有的元素重复以上的步骤,除了最后一个;
重复步骤1~3,直到排序完成。
优点:实现简单,n较小时性能较好
代码:
public static void bubbleSort(int[] arr) {
int temp = 0;
for (int i = arr.length - 1; i > 0; i--) { // 每次需要排序的长度
for (int j = 0; j < i; j++) { // 从第一个元素到第i个元素
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}//loop j
}//loop i
}// method bubbleSort`
第一种优化
/**冒泡排序(加入了判断是否已经排序了的boolean变量) */
public static void bubbleSort(int[] arr) {
for(int end = arr.length-1; end > 0; end--){
boolean isSort = true;
for(int i = 0; i < end; i++){
if(arr[i] > arr[i+1]){
swap(arr,i,i+1);
isSort = false;
}
}
if(isSort)break;
}
}
第二种优化
//记录上一次最后交换的那个位置,下一轮交换只需要进行到这个位置即可;
public static void bubbleSort2(int[] arr){
for(int end = arr.length-1; end > 0; end--){
int border = 0;
for(int i = 0; i < end; i++){
if(arr[i] > arr[i+1]){
swap(arr, i, i+1);
border = i+1;
}
}
end = border;
}
}
鸡尾酒排序
/**改进的冒泡排序(鸡尾酒排序) 就是把最大的数往后面冒泡的同时, 最小的数也往前面冒泡*/
public static void cocktailSort(int[] arr) {
int L = 0,R = arr.length-1;
while(L < R) {
for(int i = L; i < R; i++) if(arr[i] > arr[i+1]) swap(arr,i,i+1);
R--;
for(int i = R; i > L; i--) if(arr[i] < arr[i-1]) swap(arr,i,i-1);
L++;
}
}
选择排序
实现:在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕
//选择排序
public static void selectSort(int[] arr){
for(int i = 0; i < arr.length; i++) {
int minIndex = i;/**记录后面的最小值的下标*/
for (int j = i + 1; j < arr.length; j++) //注意从i+1开始
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
swap(arr,i,minIndex);
}
}
插入排序
实现:把待排序的数组分成已排序和未排序两部分,初始的时候把第一个元素认为是已排好序的。
从第二个元素开始,在已排好序的子数组中寻找到该元素合适的位置并插入该位置。
重复上述过程直到最后一个元素被插入有序子数组中。
代码:
public static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int key = arr[i], j;
for (j = i - 1; j >= 0 && key < arr[j]; j--) arr[j + 1] = arr[j]; //中间的元素往后面移动
arr[j + 1] = key; //将key插入到合适的位置
}
}
优化:二分插入排序
在前面已经排好序的序列中找当前要插入的元素的时候采用二分查找的方式去找那个插入的位置(大于key的那个位置)
//二分插入排序
public static void insertSort3(int[] arr) {
for(int i = 1; i < arr.length; i++) {
int key = arr[i];
int L = 0, R = i-1;
while(L <= R) {
int mid = L + (R-L)/2;
if(arr[mid] > key)R = mid - 1;
else L = mid + 1;
}
for(int j = i-1; j >= L; j--)arr[j+1] = arr[j];
arr[L] = key;
}
}
希尔排序
实现:
把数组分成几块,每一块进行一个插入排序;
而分块的依据在于增量的选择分好块之后,从gap开始到n,每一组和它前面的元素(自己组内的)进行插入排序;
每次和组内的元素比较完之后,最后的元素基本就是有序的了,希尔排序相对于插入排序的优势在于插入排序每次只能将数据移动一位,不过希尔排序时间复杂度的大小还是要取决于步长的合适度,另外希尔排序不是一种稳定的排序算法。
//步长为n/2....
public static void shellSort2(int[] arr) {
for(int gap = arr.length; gap > 0; gap /= 2) { //增量序列
for(int i = gap; i < arr.length; i++) { //从数组第gap个元素开始
int key = arr[i],j; //每个元素与自己组内的数据进行直接插入排序
for(j = i-gap; j >= 0 && key < arr[j]; j -= gap) arr[j+gap] = arr[j];
arr[j+gap] = key;
}
}
}
//步长为 n*3+1
public static void shellSort(int[] arr) {
int gap = 0;
for(; gap <= arr.length; gap = gap*3+1);
for(; gap > 0; gap = (gap-1)/3) { //增量序列
for(int i = gap; i < arr.length; i++) { //从数组第gap个元素开始
int key = arr[i],j; //每个元素与自己组内的数据进行直接插入排序
for(j = i-gap; j >= 0 && key < arr[j]; j -= gap) arr[j+gap] = arr[j];
arr[j+gap] = key;
}
}
}
归并排序
递归法(Top-down)
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
设定两个指针,最初位置分别为两个已经排序序列的起始位置
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针到达序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
public static void mergeSort(int[] arr) {
if(arr == null || arr.length <= 1)
return;
mergeProcess(arr,0,arr.length-1);
}
public static void mergeProcess(int[] arr,int L,int R) {
if(L >= R)
return; //递归条件判断
int mid = L + ((R - L ) >> 1); //这个相当于 (R+L)/2;
mergeProcess(arr, L, mid); //T(n/2)
mergeProcess(arr, mid+1, R); //T(n/2)
/**这个是一个优化,因为arr[L,mid]和arr[mid+1,R]已经有序,所以如果满足这个条件,就不要排序了,防止一开始数组有序*/
if(arr[mid] > arr[mid+1])
merge(arr,L,mid,R); // O(n)
//整个的表达式 T(n) = 2*T(n/2) + O(n) 使用master公式求出 N*logN
}
public static void merge(int[] arr, int L, int mid, int R) {
int[] help = new int[R-L+1];
int k = 0;
int p1 = L,p2 = mid + 1;
//while(p1 <= mid && p2 <= R)
// help[k++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
while(p1 <= mid && p2 <= R)
help[k++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++]; //左右两边相等的话,就先拷贝左边的(实现稳定性)
while(p1 <= mid) //左边剩余的部分
help[k++] = arr[p1++];
while(p2 <= R) //右边剩余的部分
help[k++] = arr[p2++];
for(int i = 0; i < k; i++) //拷贝回原来的数组
arr[i+L] = help[i];
}
自写
import java.util.*;
public class Solution {
public int[] twoSum (int[] A){
if(A==null||A.length==0) return null;
int[] temp= new int[A.length];
mergeSort(A,0,A.length-1,temp);
return temp;
}
private void mergeSort(int[] A,int start,int end,int[] temp){
if(start>=end) return;
mergeSort(A,start,(start+end)/2,temp);
mergeSort(A,(start+end)/2,end,temp);
merge(A,start,(start+end)/2,temp);
}
private void merge(int[] A,int start,int end,int[] temp){
int middle=(start+end)/2;
int left=start;
int right=middle+1;
int index=left;
while(left==middle&&right==end){
if(A[left]<A[right]){
temp[index++]=A[left++];
}else{
temp[index++]=A[right++];
}
}
while(left<=middle){
temp[index++]=A[left++];
}
while(right>=end){
temp[index++]=A[right++];
}
}
}
两个与之相关的算法题:归并排序求解逆序数,小和问题
迭代法(Bottom-up)
将序列每相邻两个数字进行归并操作,形成ceil(n/2)个序列,排序后每个序列包含两/一个元素
若此时序列数不是1个则将上述序列再次归并,形成ceil(n/4)个序列,每个序列包含四/三个元素
重复步骤2,直到所有元素排序完毕,即序列数为1
public class BUMergeSort {
private static int[] aux;
public BUMergeSort(int[] a) {
sort(a);
}
private static void sort(int[] a) {
int N = a.length;
aux = new int[N];
for(int sz = 1;sz < N;sz = sz+sz) {
for(int lo = 0;lo < N-sz;lo+=sz+sz) {
merge(a,lo,lo+sz-1,Math.min(lo+sz+sz-1,N-1));
}
}
}
private static void merge(int[] a, int lo,int mid,int hi) {
int i = lo,j = mid+1;
for(int k = lo; k<=hi;k++) {
aux[k] = a[k];
}
for(int k = lo;k <=hi;k++) {
if(i > mid) {
a[k] = aux[j++];
}
else if(j > hi) {
a[k] = aux[i++];
}
else if(less(aux[j],aux[i])) {
a[k] =aux[j++];
}
else {
a[k] = aux[i++];
}
}
}
private static boolean less(int a,int b) {
if(a < b) {
return true;
}else {
return false;
}
}
}
快速排序
实现
从数列中挑出一个元素,称为 “基准”(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个 分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
void quick_sort(int s[], int l, int r)
{
if (l < r)
{
//Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1
int i = l, j = r, x = s[l];
while (i < j)
{
while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
j--;
if(i < j)
s[i++] = s[j];
while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
i++;
if(i < j)
s[j--] = s[i];
}
s[i] = x;
quick_sort(s, l, i - 1); // 递归调用
quick_sort(s, i + 1, r);
}
}
public String solve (int[] A) {
quickSort(A,0,A.length-1);
}
private void quickSort(int[] A,int start,int end){
if(start>=end) return;
int left=start,right=end;
int pivot=A[(start+end)/2];
while(left<=right){
while(left<=right&&A[left]<pivot){
left++;
}
while(left<=right&&A[right]>pivot){
right--;
}
if(left<=right){
int temp=A[left];
A[left]=A[right];
A[right]=temp;
left++;
right--;
}
}
quickSort(A,start,right);
quickSort(A,left,end);
}
优化:三路快速排序
public static int[] partition(int[] arr,int L,int R,int num){
int less = L-1; //小于部分的最后一个数
int more = R+1;
int cur = L;
while( cur < more){
if(arr[cur] < num){
swap(arr,++less,cur++); //把这个比num小的数放到小于区域的下一个,并且把小于区域扩大一个单位
}else if(arr[cur] > num){
swap(arr,--more,cur); //把这个比num大的数放到大于去余的下一个,并且把大于区域扩大一个单位
//同时,因为从大于区域拿过来的数是未知的,所以不能cur++ 还要再次判断一下arr[cur]
}else{// 否则的话就直接移动
cur++;
}
}
return new int[]{less + 1,more - 1}; //返回的是等于区域的两个下标
}
应用:荷兰国旗问题
堆排序
思想:
将待排序列构造成一个大顶堆(或小顶堆),整个序列的最大值(或最小值)就是堆顶的根结点
将根节点的值和堆数组的末尾元素交换,此时末尾元素就是最大值(或最小值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次大值(或次小值),如此反复执行,最终得到一个有序序列。
基本的堆排序:
public static void heapSort(int[] arr){
if(arr == null || arr.length <= 1)return ;
for(int i = 0; i < arr.length; i++){
siftUp(arr,i);
}
int size = arr.length;
swap(arr,0,--size);
while(size > 0){
siftDown(arr,0,size);
swap(arr,0,--size);
}
}
//上浮的过程 --> 把新插入的数调整为大根堆的过程
public static void siftUp(int[] arr,int i){
while(arr[i] > arr[(i-1)/2]){
swap(arr,i,(i-1)/2);
i = (i-1)/2;
}
}
//下沉的过程 --> 这个函数就是一个数变小了,往下沉的函数,改变的数为index 目前的自己指定的堆的大小为heapSize
public static void siftDown(int[] arr,int i,int heapSize){
int L = 2*i+1;
while(L < heapSize){
int maxIndex = L+1 < heapSize && arr[L+1] > arr[L] ? L+1 : L;
maxIndex = arr[i] > arr[maxIndex] ? i : maxIndex;
if(maxIndex == i)break; //自己就是最大的, 不用忘下沉
//否则就要一直往下沉
swap(arr,i,maxIndex);
i = maxIndex;
L = 2*i + 1; //继续往下
}
}
优化:
里建堆的时候没有使用上浮,而是从第一个非叶子结点开始使用下沉的方式建堆
public static void heapSort(int[] arr){
if(arr == null || arr.length <= 1)return ;
int size = arr.length - 1;
for(int i = (size - 1)/2; i >= 0; i--)
siftDown(arr,i,size+1);
swap(arr,0,size);
while(size > 0){
siftDown(arr,0,size);
swap(arr,0,--size);
}
}
/**往下沉的函数,改变的数为index 目前的自己指定的堆的大小为heapSize */
public static void siftDown(int[] arr,int i,int heapSize){
int L = 2*i+1;
while(L < heapSize){
int maxIdx = L+1 < heapSize && arr[L+1] > arr[L] ? L+1 : L;
maxIdx = arr[i] > arr[maxIdx] ? i : maxIdx;
if(maxIdx == i)break; //自己就是最大的, 不用忘下沉
swap(arr,i,maxIdx);
i = maxIdx;
L = 2*i + 1; //继续往下
}
}
//shiftDown的过程也可以递归的换:
//递归的调整A[i]以下的堆
public static void siftDown(int[] arr,int i,int size) { //从A[i] 开始往下调整
int L = 2*i+1; //左孩子的下标
int R = 2*i+2;//右孩子的下标
int maxIdx = i;
if(L < size && arr[L] > arr[maxIdx])maxIdx = L;
if(R < size && arr[R] > arr[maxIdx])maxIdx = R;
if(maxIdx != i) {
swap(arr,i,maxIdx); //把当前结点和它的最大(直接)子节点进行交换
siftDown(arr,maxIdx,size); //继续调整它的孩子
}
}