快速排序
思路:随机选择数组中的一个数x和数组最后一个数进行交换,然后用随机选择的数x(即数组末位数值)来作为枢轴进行划分。这样的话,就不能说会轻易的找到最差的情况了,因为每一次排序选择的枢轴都是随机的,可能选择到最好情况的枢轴(中位数),也有可能选择到最差情况的枢轴(边界值),这样的话,它的复杂度就是一个概率事件了。所以在估计的时候就不能说它有一个最差情况了,只能用长期期望的方式来算出它的时间复杂度。这里有计算公式证明长期期望的时间复杂度是O(NlogN)。空间复杂度为O(logN)。空间复杂度浪费在了划分点p的位置上,每一次排序都要记下划分点位置,方便找到递归子过程的边界位置。
public class QuickSortTest {
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 = patition(arr, L, R);
quickSort(arr, L, p[0]-1);
quickSort(arr, p[1]+1, R);
}
}
public static int[] patition(int[] arr, int L, int R) {
int less = L - 1;
int more = R;
int cur = L;
while(cur < more) {
if(arr[cur] < arr[R]) {
swap(arr, ++less, cur++);
} else if(arr[cur] > arr[R]) {
swap(arr, --more, cur);
} else {
cur++;
}
}
swap(arr, more, R);//当遍历结束之后(指针相遇),将枢轴x与大于x的第一位上的数(数组下标为more的数)进行交换
return new int[] {less+1,more};//less+1和more为数组中等于x的数在排序后的下标的左右临界位置
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
直接插入排序
基本思想:把n个待排序的元素看成是一个有序表和一个无序表,开始的时候,有序表中只有一个元素(待排序数组的首个元素),无序表中有n-1个元素;排序过程就是每次从无序表中取第一个元素,插入有序表中,通过比较大小重排序,使之成为新的有序表,重复n-1次完成整个排序过程。
public class InsertSortTest {
public static void insertSort(int[] arr) {
if(arr == null || arr.length <= 1) return;
for(int i = 1; i < arr.length; i++) { // 遍历无序表
int temp = arr[i], j = i; // temp为无序表的第一个元素,j为无序表的第一个下标
if(arr[j-1] > temp) {
while(j >= 1 && arr[j-1] > temp) {
arr[j] = arr[j-1]; // 移动有序表中元素位置,将当前元素插入有序表中正确位置
j--;
}
}
arr[j] = temp; // 插入有序表正确位置
}
}
}
归并排序
思想:归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
public class MergeSortTest {
public static void mergeSort(int[] arr) {
int[] temp = new int[arr.length]; // 在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
sort(arr, 0, arr.length-1, temp);
}
private static void sort(int[] arr, int left, int right, int []temp) {
if (left < right) {
int mid = (left+right) / 2;
sort(arr, left, mid, temp); // 左边归并排序,使得左子序列有序
sort(arr, mid+1, right, temp); // 右边归并排序,使得右子序列有序
merge(arr, left, mid, right, temp); // 将两个有序子数组合并操作
}
}
private static void merge(int[] arr, int left, int mid, int right, int[] temp){
int i = left; // 左序列指针
int j = mid+1; // 右序列指针
int t = 0; // 临时数组指针
while (i <= mid && j <= right){
if (arr[i] <= arr[j]){
temp[t++] = arr[i++];
} else {
temp[t++] = arr[j++];
}
}
while (i <= mid) { //将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while (j <= right){//将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0; //因为需要拷贝所以要把临时数组的指针置0(即指向第一个元素)
// 将temp中的元素全部拷贝到原数组中
while (left <= right){
arr[left++] = temp[t++];
}
}
}
该算法的最优时间复杂度和最差时间复杂度以及平均复杂度是一致的:O(nlogn)。
堆排序
思路:
1)先让这个数组整体变成一个大顶堆(变成大顶堆的数组并不一定会有序),然后把最后一个位置的数与堆顶位置的数做交换,堆顶位置的数一开始是数组的最大值,换完之后最大值就来到了数组最后的位置,此时让堆大小heapSize减1,最大值就永远不动了。
2)在剩下的范围中,从0开始做heapify的下沉操作重新调整成大顶堆。然后把调整好的堆顶位置的数再与现在的最后一个位置的数交换,此时,倒数第二个位置上的数就是第二大的数了。
3)每次搞定一个末尾的数,再让heapSize减1,以此类推,减完之后,整个数组就是有序的了。
public class HeapSortTest {
public static void heapSort(int [] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length; i++) {
heapInsert(arr,i); // 依次将数组中i位置上的数加进来,让它0~i之间的数形成大顶堆
}
int heapSize = arr.length; // 堆大小heapSize一开始等于数组的全部
swap(arr, 0, --heapSize); // 最后一个位置上的数与第一个位置上的数(堆顶元素)交换,堆大小减1,即最后一个位置上的数不动了
while (heapSize > 0) {
heapify(arr, 0, heapSize); // 从0位置开始,将当前形成的堆继续调整为一个大顶堆
swap(arr, 0, --heapSize); // 最后一个位置上的数与第一个位置上的数(堆顶元素)交换,堆大小减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; // 然后index来到了它的父节点位置,继续上面的while
}
}
// 若有一个节点的值变小了,则需要往下沉(与其左右孩子中较大的数进行交换位置的)的操作
public static void heapify(int[] arr, int index, int heapSize) {
int left = index*2 + 1; // 左孩子下标
while (left < heapSize) { // 未越界,即该节点并非叶子节点,存在左孩子
// 该节点有右孩子,将左右孩子较大值的下标赋值给largest
int largest = (left+1 < heapSize) && arr[left+1] > arr[left] ? left+1 : left;
// 将该节点与该节点左右孩子中较大值的下标赋值给largest(这两步相当于从当前节点与其子节点中选出最大者的下标赋值给largest)
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index); // largest != index
index = largest; // 该节点下标变成了较大孩子的下标
left = index*2 + 1; // 继续往下走,重复上面的while
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
冒泡排序
思想:依次比较相邻的两个数,将小数放在前面,大数放在后面。
即在第一趟:首先比较第1个和第2个数,将小数放前,大数放后。
然后比较第2个数和第3个数,将小数放前,大数放后。
如此继续,直至比较最后两个数,将小数放前,大数放后。
重复第一趟步骤,直至全部排序完成。
规律:N个数字要排序完成,总共进行N-1趟排序,第i趟的比较次数为(N-i)次,所以可以用双重循环语句,外层控制循环多少趟,内层控制每一趟的循环次数。每一趟排序都能确定一个最大值排在数组末尾,所以x趟排序可以确定x个末端的数值是在正确的位置上的。
public class BubbleSortTest {
static void BubbleSort(int s[]) {
for (int i = 0; i < s.length - 1; i++) {
for (int j = 0; j < length - 1 - i; j++) {
if (s[j] > s[j+1]) {
int temp = s[j];
s[j] = s[j+1];
s[j+1] = temp;
}
}
}
}
}
如果数据是正序的,只需要走一趟即可完成排序。所需的比较次数最小为n-1,移动次数最小为0。
所以,冒泡排序最好的时间复杂度为O(n)。
如果数据是反序的,则需要进行n-1趟排序。每趟排序要进行n-1-i次比较,且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较最大为n(n-1)/2,移动次数最大值为3n(n-1)/2。
所以,冒泡排序最坏的时间复杂度为O(n^2)。
希尔排序
思想:希尔排序也称为“缩小增量排序”,基本原理如下:先将待排序的数组元素分成多个子序列,使得每个子序列的元素个数相对较少,然后对各个子序列分别进行直接插入排序,待整个排序序列“基本有序后”,最后再对所有元素进行一次直接插入排序。
public class ShellSortTest{
public static void sort(int[] arr) {
if(arr == null || arr.length <= 1) {
return;
}
int i, j;
int increment;
int temp;
for (increment = arr.length / 2; increment > 0; increment /= 2) {
for (i = increment; i < arr.length; i++) {
temp = arr[i];
for (j = i - increment; j >= 0; j -= increment) {
if (temp < arr[j]) {
arr[j+increment] = arr[j];
} else
break;
}
arr[j+increment] = temp;
}
}
}
}