转载请注明出处:http://blog.csdn.net/pngfi/article/details/52154785
选择排序要用到交换,在开始之前不妨说下数值交换的三种方法
public static void swap(int[] arr, int i, int j) {
if (i != j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
public static void swap(int[] arr, int i, int j) {
if (i != j) {
arr[i] = arr[i] + arr[j];
arr[j] = arr[i] - arr[j];
arr[i] = arr[i] - arr[j];
}
}
public static void swap(int[] arr, int i, int j) {
if(i!=j){
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
}
简单选择排序思路很简单,就是每次遍历数组获得最小值得位置,然后将最小值与数组中第一个值交换,然后从第二个值开始遍历获得最小值与第二个元素交换,以此类推。
public static void selectSort(int[] arr) {
int min;
for (int i = 0; i < arr.length; i++) {
min = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
swap(arr, min, i);
}
}
最好最、坏时间复杂度都是O(n²)。内层for循环执行次数依次为n-1,n-2,n-3,……,1。加起来为n(n-1)/2 。 该算法是不稳定的,很容易理解,由于交换的时候,会改变两个相等值得相对位置。
将序列中的第一个元素作为一个有序序列,然后将剩下的n-1个元素按照关键字大小依次插入该有序序列,每插入一个元素后依然保持该序列有序,经过n-1趟排序后即成为有序序列。
public static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int j = i;
int temp = arr[i];
while (j > 0 && temp < arr[j - 1]) {
arr[j] = arr[j - 1];
j--;
}
arr[j] = temp;
}
}
算法必须进行n-1趟。最好情况下,每趟都比较一次,时间复杂度为O(n);最坏情况下的时间复杂度为O(n²)。 插入排序是稳定的,当然这与循环条件中”temp < arr[j - 1]”密切相关,如果改为”temp < =arr[j - 1]” 那么就不稳定了。
第一趟在数组(arr[0]-arr[n-1])中从前往后进行两个相邻元素的比较,若后者小,则交换,比较n-1次;第一趟排序结束,最大的元素到 arr[n-1] 中 ;下一趟排序在子数组arr[0]-arr[n-2]中进行;如果在某一趟排序中未交换元素,说明子序列已经有序,不需要再进行下一趟排序。
public static void bubbleSort(int[] arr) {
int i = arr.length - 1;
int last;//记录某趟最后交换的位置,其后的元素都是有序的
while (i > 0) {
last=0;
for (int j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
last= j;
}
}
i=last;
}
}
其中变量last主要是要记录,某趟遍历时最后一次交换的位置。在last之后的元素其实都已经在排序结果中正确的位置。后面再进行下一趟时,只要从arr[0]到arr[last] 进行操作即可。最终当last=0时,就说明arr[0]之后的元素都已经在正确的位置了,那么只剩arr[0]肯定也是在正确的位置。
该算法最多进行n-1趟。冒泡排序在已排序的情况下是最好情况,只用进行一趟,比较n-1次,时间复杂度为O(n) ; 最坏情况下要进行n-1趟,第i每次比较n-i次,移动3(n-i)次,时间复杂度为O(n²); 冒泡排序是稳定的排序算法,这当然也与算法中的判断条件相关。
快排简略来说就是一种分治的思想。对一个数组,选定一个基准元素x,把数组划分为两个部分,低端部分都比x小,高端元素都比x大,那么此时基准元素就在正确的位置了。然后再分别都低端部分和高端部分,分别进行上面的步骤,直到子数组中只有一个元素为止。
递归版
public static void quickSort(int[] arr) {
qSort(arr, 0, arr.length - 1);
}
private static void qSort(int[] arr, int left, int right) {
//如果分割元素角标p为子数组的边界,那么分割后就只有低端序列或者高端序列,此时判断防止角标越界
if(left==right){
return;
}
int p = partition(arr, left, right);
qSort(arr, left, p - 1);
qSort(arr, p+1,right);
}
/**
* 把数组分为两部分
*
* @param arr
* @param left
* 子数组最小角标
* @param right
* 子数组最大角标
* @return 返回分割元素的角标
*/
private static int partition(int[] arr, int left, int right) {
int base = arr[right];
int small=left-1;//用来记录小于base的数字放到左边第几个位置
for(int i=left;iif(arr[i] return small;
}
非递归版
public static void quickSort(int[] arr){
Deque leftStack=new ArrayDeque();
Deque rightStack=new ArrayDeque();
leftStack.addFirst(0);
rightStack.addFirst(arr.length-1);
while(leftStack.size()!=0){
Integer left = leftStack.removeFirst();
Integer right = rightStack.removeFirst();
int p = partition(arr, left, right);
if(p>left){
leftStack.addFirst(left);
rightStack.addFirst(p-1);
}
if(p<right){
leftStack.addFirst(p+1);
rightStack.addFirst(right);
}
}
}
上面partition函数中每次选择子数组中第一个元素作为基准元素,当然你也可以选择其他的。
当初始数组有序(顺序或逆序)时,快排效率最低,因为每次分割后有一个子数组是空,时间复杂度是O(n²);在平均情况下可以证明快排的时间复杂度是O(nlogn)。
在最坏的情况下空间复杂度是O(n),最好的情况下是O(logn)。
我们可以看出无论是递归还是非递归,快排至少O(logn)的空间复杂度,这个是省不掉的。
快排是不稳定的。
把n个长度的数组看成是n个长度为1的数组,然后两两合并时排序,得到n/2个长度为2的数组(可能包含一个1); 继续两两合并排序,依次类推。
归并排序算法的核心是合并,我具体来说一下合并的思路。比如我们有数组A,B,此时我们要在合并时排序,需要一个临时数组C。用leftPosition,rightPosition和tempPositon 分别来指示数组元素,它们的起始位置对应数组的始端。A[leftPostion]和B[rightPosition]中较小者被拷贝到C中tempPostion的位置。相关的指示器加1。当A和B中有一个用完时,另一个数组中的剩余部分直接拷贝到C中。
代码如下:
/**
* @param arr
* 数组
* @param tempArr
* 临时数组,用于临时存放合并后的结果
* @param leftPostion
* 第一个子数组的开始
* @param rightPosition
* 第二个字数组的开始位置
* @param rightEnd
* 第二个子数组的结束位置
*/
private static void merge(int[] arr, int[] tempArr, int leftPostion,
int rightPosition, int rightEnd) {
int leftEnd = rightPosition - 1;
int tempPositon = leftPostion;
int begin=leftPostion;
while (leftPostion <= leftEnd && rightPosition <= rightEnd) {
if(arr[leftPostion]else {
tempArr[tempPositon++]=arr[rightPosition++];
}
}
while(leftPostion<=leftEnd){
tempArr[tempPositon++]=arr[leftPostion++];
}
while(rightPosition<=rightEnd){
tempArr[tempPositon++]=arr[rightPosition++];
}
//复制到原数组
for(int i=begin;i<=rightEnd;i++){
arr[i]=tempArr[i];
}
}
private static void mergeSort(int[] arr, int[] tempArr, int left, int right) {
if(left//如果只有一个子数组只有一个元素就直接返回
int mid=(left+right)/2;
mergeSort(arr,tempArr,left,mid);
mergeSort(arr,tempArr,mid+1,right);
merge(arr, tempArr, left, mid+1, right);
}
}
public static void mergeSort(int[] arr) {
int[] tempArr=new int[arr.length];
mergeSort(arr,tempArr,0,arr.length-1);
}
下面我们来证明一下归并排序的时间复杂度,我们假设元素个数n是2的幂,这样总是能将数组分为相等的两部分。
当n=1,归并排序的时间 T(1)=1 ;
对于任意n个元素归并排序的时间是两个 n2 n 2 大小归并排序的时间加上合并的时间。容易看出合并的时间是线性的,因为合并连个数组时,最多进行N-1比较,即每比较依次肯定会有一个数加入到临时数组中去的。
T(n)=T( n2 n 2 )+n
等式两边同除以n,
T(n)n T ( n ) n = T(n/2)n/2 T ( n / 2 ) n / 2 +1
该方程对2的幂的任意n都是成立的,我们每次除2可以得到下面的一系列等式:
T(n/2)n/2 T ( n / 2 ) n / 2 = T(n/4)n/4 T ( n / 4 ) n / 4 +1
T(n/4)n/4 T ( n / 4 ) n / 4 = T(n/8)n/8 T ( n / 8 ) n / 8 +1
…
T(2)2 T ( 2 ) 2 = T(1)1 T ( 1 ) 1 +1
明显这些等式一共有logn个。
然后把所有这些等式相加,消去左边和右边相等的项,所得结果为 T(n)n T ( n ) n = T(1)1 T ( 1 ) 1 +logn
稍作变为T(n)=nlogn+n=O(nlogn)
另外归并排序需要O(n)的空间复杂度,是一种稳定的排序算法。
将初始数组构造成最大堆,第一趟排序,将堆顶元素arr[0]和堆底元素arr[n-1]交换位置,然后将再将arr[0]往下调整,使得剩余的n-1个元素还是堆;第i趟时,arr[0]与arr[n-i]交换,arr[0]往下调整,使得剩余的n-i个元素还是堆;直到堆中只剩一个元素结束。
public static void heapSort(int[] arr) {
int n = arr.length;
// 构建初始堆
for (int i = (n - 1) / 2; i >= 0; i--)
percDown(arr, i, n - 1);
// 排序,其实相当于每次删除最大元素,然后将剩下的最后一个元素换到0位置,重新调整
for (int i = 1; i < n; i++) {
swap(arr, 0, n - i);
percDown(arr, 0, n - i - 1);
}
}
/**
* @param arr
* 数组
* @param i
* 要调整的那个元素的位置
* @param n
* 堆最后一个元素的角标
*/
private static void percDown(int[] arr, int i, int n) {
int temp = arr[i];
int child = 2 * i + 1;
while (child <= n) {
if (child < n && arr[child] < arr[child + 1])
child++;
if (temp < arr[child]) {
arr[i] = arr[child];
i = child;// 让i指向当前child的位置
child = 2 * i + 1;// 获得新的child
}else{
break;
}
}
arr[i] = temp;
}
percDown函数时间复杂度不超过O(logn),构造堆的最多时间为O(nlogn)。排序部分进行n-1趟,也是O(nlogn),所以总的时间复杂度还会O(nlogn)。
一趟排序可以确定一个元素的最终位置,堆排序是不稳定的排序算法。
简单来说就是分组插入排序,它通过比较相距一定间隔的元素来工作;各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止,因此也叫作缩减增量排序。
关于增量序列h1,h2,h3,….,ht 只要是h1=1等任何增量序列都是可行的,因为最后一趟h1=1,那么进行的工作就是插入排序啊。
看到这里你也许会好奇,其实希尔排序最后一趟和插入排序做的工作就是一模一样啊,为什么在选取某些增量序列,比如Hibbard增量(1,3,7,…, 2k 2 k -1)的情况下,时间复杂度为O( n32 n 3 2 ) 。
插入排序的性质:
这里还是选用很常见的序列 ht=N/2,hk=hk+1/2,选用这个增量序列,算法复杂度为O( n2 n 2 )。
代码如下:
public static void shellSort(int[] arr) {
for (int gap = arr.length / 2; gap > 0; gap = gap / 2) {
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[i];
while (j >= gap && temp < arr[j - gap]) {
arr[j] = arr[j - gap];
j = j - gap;
}
arr[j] = temp;
}
}
}
我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。