1.选择排序
思想:把第一个数据跟数组的其他数据比较一遍,选出最小数,放在第一个位置,然后第二个位置的数据跟数组的其他数据比较一遍,选出最小数,放在第二个位置。以此类推。
代码:从小到大排序 ,时间复杂度o(n^2)
for(int i = 0; i<t-1 ; i++){
int min = i;
for(int j=i+1; j<t; j++){
min = nums1[min] > nums1[j] ? j : min;
}
if(min != i){
int swap = nums1[i];
nums1[i] = nums1[min];
nums1[min] = swap;
}
}
2.冒泡排序
思想:比较相邻的两个数,左边的数大于右边的数则交换,遍历完最大的数会在最后一个位置,接下来第二遍遍历的长度减1,依次比较相邻的两个数并交换,以此类推。
代码:从小到大排序,时间复杂度o(n^2)
for(int i = 0; i<t; i++){
for(int j=0; j<t-i-1; j++){
if(nums1[j] > nums1[j+1]){
int swap = nums1[j];
nums1[j] = nums1[j+1];
nums1[j+1] = swap;
}
}
}
3.插入排序
思想:将一个记录插入到已经排好序的有序表中,从而行程一个完整的,记录+1的有序表。
代码:从小到大排序,时间复杂度 o(N^2)。
for(int i = 1; i<t; i++){
for(int j=i-1; j>=0 && nums1[j+1] < nums1[j]; j--){
int swap = nums1[j+1];
nums1[j+1] = nums1[j];
nums1[j] = swap;
}
}
4.归并排序
思想:简单的一个递归,左边排好序,右边排好序,将有序表合并成一个有序表。
代码:时间复杂度O(nlogn),空间复杂度T(n)。
//两个有序表合并
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = 0;
int p1 = 0;
int p2 = 0;
int[] help = new int[m+n];
while(p1 <= m-1 && p2 <= n-1){
help[i++] = nums1[p1] <= nums2[p2] ? nums1[p1++] : nums2[p2++];
}
while(p1<= m-1){
help[i++] = nums1[p1++];
}
while(p2 <= n-1){
help[i++] = nums2[p2++];
}
for(int j = 0 ; j< m+n ; j++){
nums1[j] = help[j];
}
}
5.快速排序
思想:采用的分治的思想,随机选择一个数作为基准,数组中左边的部分小于基准数,右边的数据大于基准数,然后采用递归的方式对左右部分重复上述操作。
代码:时间复杂度O(NlogN)。
public static void quickSort(int[] arr,int l, int r){
if(l >= r){
return;
}
swap(arr, r,l+(int)(Math.random()*(r-l+1)));
int[] partition = partition(arr, l, r);
quickSort(arr, l, partition[0]);
quickSort(arr, partition[1], r);
}
public static int[] partition(int[] arr,int l, int r){
int p1 = l-1;
int p2 = r;
for (int i = l; i < p2; i++) {
if(arr[i] < arr[r]){
swap(arr, ++p1, i);
}else if(arr[i] > arr[r]){
swap(arr, --p2, i--);
}
}
swap(arr, p2, r);
return new int[]{p1, p2+1};
}
public static void swap(int[] arr,int position1, int position2){
if(position1 != position2){
arr[position1] = arr[position1]^ arr[position2];
arr[position2] = arr[position1]^ arr[position2];
arr[position1]= arr[position1]^ arr[position2];
}
}
6.堆排序
堆排序是指利用堆这种数据结构所涉及的排序算法。(二叉)堆是一个数组,它可以被看成是一个近似的完全二叉树,二叉堆分为两种形式,最大堆和最小堆,最大堆是指父节点大于等于子节点,最大堆的最大元素在根节点。最小堆是父节点小于等于子节点,最小堆的最小元素在根节点。堆排序算法中用的是最大堆,最小堆通常用于构造优先队列。
6.1维护堆的性质
public static void heapify(int[] data, int i){
int max = i;
int l = 2*i+1;
int r = 2*i+2;
if(l<= data.length-1 && data[l] > data[i]){
max = l;
}
if(r<= data.length-1 && data[r] > data[max]){
max = r;
}
if(max != i){
data[max] = data[i] ^ data[max];
data[i] = data[i] ^ data[max];
data[max] = data[i] ^ data[max];
heapify(data, max);
}
}
上个方法是对于一棵以i为节点,大小为n的数,除了i节点以外的节点都是符合最大堆,把i节点和其他节点变成最大堆。上面方法的时间代价包括:调整A[i]和A[max]为(1),和以i为根节点的子树的方法运行时间。每个孩子的子树最大为2/3n(最坏情况在树的叶子节点半满时),所以可以用递归来刻画运行时间:T(n) T(2/3n) + (1)。根据master公式得出 O(lgn)。
6.2建堆
存在一个数组,可以用heapify方法,把除了叶子节点的每一个节点自右而左,自下而上遍历。使其成为最大堆。
public static void buildMaxHead(int[] data){
for (int i = data.length/2; i>=0; i--){
heapify(data, i);
}
}
每次调用heapify的时间复杂度是O(lgn),需要调用O(n)次,所以总的时间复杂度为O(nlgn)。这个上界虽然正确但不是渐近紧确的。
可以观察到不同节点运行heapify的时间与该节点的树高有关系,而且大部分节点的高度很小。利用下面性质可以得到更紧确的界:包含n个元素的堆的高度为[lgn],高度为h的堆最多包含[n/]个节点。
在一个高度为h的节点上运行heapify的代价是O(h),可以将buildMaxHead的总代价表示为:
O(h) = O(1/2*n) = O(n)。下面是详细计算过程。
对于x1, = 1+ ++...+ = 。
当n是无限的,且|x|<1时,几乎为0,所以 = 。
对上面公式进行微分,可以得到 = 。下面是详细计算。
O(1/2*n)是以x=代入上面公式计算得出 = 2。所以得出O(n)。
6.3堆排序算法
堆排序算法的思想是:构建一个最大堆,然后把最大的元素跟最后一个元素互换后,把堆的节点数量减1,然后剩下的节点执行heapfipy,重复上面过程完成排序。因为构建最大堆的时间复杂度为O(n),然后互换元素后heapfipy的时间复杂度为O(lgn),然后会调用n-1次heapfipy。所以时间复杂度为O(nlgn)。
以上几种算法的对比。
算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
选择排序 | O(n2) | O(1) | 不稳定 |
冒泡排序 | O(n2) | O(1) | 稳定 |
插入排序 | O(n2) | O(1) | 稳定 |
归并排序 | O(nlgn) | O(n) | 稳定 |
快速排序 | O(nlgn) | O(lgn) | 不稳定 |
堆排序 | O(nlgn) | O(1) | 不稳定 |