一、相关归纳总结
1.时间复杂度
O(n^2):冒泡排序,选择排序,插入排序
O(log2n):快速排序,希尔排序,堆排序,归并排序
2.空间复杂度
O(1):插入排序、冒泡排序,选择排序,堆排序,希尔排序
O(log2n)~O(n):快速排序
O(n):归并排序
3.稳定性
若待排序的序列中,存在多个相同关键字的记录,经过排序,这些记录的相对次序保持不变,则称该算法是稳定的;若经过排序后,记录的相对次序发生了改变,则称该算法是不稳定的。
稳定的:冒泡排序,插入排序,归并排序和基数排序
不稳定的:选择排序,快速排序,希尔排序,堆排序
1.冒泡排序
基本思想:
假设数组长度为length,从前往后(或从后往前)两两比较相邻元素的值,若为逆序(即arr[i]>arr[i+1]),则交换他们,直到数组比较完,我们称它为一趟冒泡,会将最大的元素交换到最后一个位置。下一趟冒泡时,前一趟确定的最大元素不再参加比较,待排序列减少一个元素,每趟冒泡的结果把数组中的最大元素放在最终位置。这样最多做n-1趟冒泡就能把所有元素排好序。
代码优化:
加入swap判断语句,若遍历的数组元素以有序,则直接跳出循环减少遍历次数,减少时间,因此时间复杂度最好O(n)。
时间复杂度:O(n^2),空间复杂度:O(1) 稳定性:很稳定
代码实现:
public class bubbleSort {
private static void bubbleSort(int [] arr){
boolean swap = false;//定义swap判断是否近入到了循环,本来就是从小到大顺序,则不进入
for (int i = 0; i < arr.length-1; i++) {//遍历趟数
for (int j = 0; j < arr.length-1-i; j++) {//遍历一次
if (arr[j] > arr [j+1]){
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
swap = true;
}
}
if (!swap){
break;
}
}
}
测试代码:
public static void main(String[] args) {
int[] arr = new int[]{12,5,7,8,54,32,13};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
2.选择排序
基本思想:
给定数组:int[] arr={里面n个数据};第1趟排序,在待排序数据arr[1]—arr[n]中选出最小的数据,将它与arrr[1]交换;第2趟,在待排序数据arr[2]—arr[n]中选出最小的数据,将它与r[2]交换;以此类推,第i趟在待排序数据arr[i]~arr[n]中选出最小的数据,将它与r[i]交换,直到全部排序完成。 ( 每一趟从待排序的记录中选出最小的元素,顺序放在已排好序的序列最后,直到全部记录排序完毕。也就是:每一趟在n-i+1(i=1,2,…n-1)个记录中选取关键字最小的记录作为有序序列中第i个记录。基于此思想的算法主要有简单选择排序、树型选择排序和堆排序。)
时间复杂度:O(n^2),空间复杂度:O(1) 稳定性:不稳定
代码实现:
private static void choiceSort(int[] arr) {
for(int i=0; i<arr.length-1; ++i) {// 做第i趟排序
int min = arr[i];
int k = i;
for(int j=i+1; j<arr.length; ++j) {// 选最小的记录
if(min > arr[j]){
min = arr[j];
k = j;//记下目前找到的最小值所在的位置
}
}
//在内层循环结束,也就是找到本轮循环的最小的数以后,再进行交换
if(k != i) {//交换a[i]和a[k]
int tmp = arr[i];
arr[i] = arr[k];
arr[k] = tmp;
}
}
}
public static void main(String[] args) {
int[] arr = new int[]{12, 5, 7, 8, 54, 32, 13};
choiceSort (arr);
System.out.println (Arrays.toString (arr));
}
}
3.插入排序
基本思想:
利用插入法对无序数组排序时,其实是将数组arr划分成两个子区间arr[1..i-1](已排好序的有序区)和arr[i..n](当前未排序的部分,可称无序区)。插入排序的基本操作是将当前无序区的第1个记录arr[i]插人到有序区arr[1..i-1]中适当的位置上,使arr[1..i]变为新的有序区。因为这种方法每次使有序区增加1个记录,通常称增量法。
时间复杂度:O(n^2), 空间复杂度:O(1) 稳定性:稳定
最好情况:正序,不需要移动元素 最差情况:反序,需要移动n*(n-1)/2个元素
数组在已排序或者是“近似排序”时,插入排序效率的最好情况运行时间为O(n);
插入排序最坏情况运行时间和平均情况运行时间都为O(n^2)。
时间复杂度最好:O(n) 最坏 :O(n^2) 退换成冒泡排序 空间复杂度:O(1) 不稳定
代码实现:
private static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; ++i) {
int val = arr[i];
// 在i前面已经排序好的序列中,找i元素插入的合适的位置
int j = i - 1;
// 0 j 找第一个小于val的值 线性查找
for(; j>=0; --j){
if(val < arr[j]) {
arr[j+1] = arr[j];
}else {
break;
}
}
// 把val元素插入到当前位置,然后循环结束
arr[j + 1] = val;
}
}
public static void main(String[] args) {
int[] arr = new int[]{12, 5, 7, 8, 54, 32, 13};
insertSort (arr);
System.out.println (Arrays.toString (arr));
}
}
代码优化:化成二分查找合适的插入位置,不再使用线性查找,减少遍历次数,节省时间
private static void insertSort(int[] arr) {
for(int i=1; i<arr.length; ++i) {
int val = arr[i];
// 优化成二分查找合适的插入位置,不再使用线性查找
// 0 j 找第一个小于val的值
int index = findInsertPos(arr, 0, i-1, val);
for(int j=i; j>index; --j){
arr[j] = arr[j-1];
}
arr[index] = val;
}
}
private static int findInsertPos(
int[] arr, int i, int j, int val) {
while(i <= j){
int mid = (i+j)/2;
if(val < arr[mid]) {
j = mid-1;
} else {
i = mid+1;
}
}
return i;
}
public static void main(String[] args) {
int[] arr = new int[]{12, 5, 7, 8, 54, 32, 13};
insertSort (arr);
System.out.println (Arrays.toString (arr));
}
}
4.快速排序
基本思想:
选择一个关键值作为基准值。比基准值小的都在左边序列(一般是无序的),比基准值大的都在右边(一般是无序的)。一般选择序列的第一个元素。
一次循环:从后往前比较,用基准值和最后一个值比较,如果比基准值小的交换位置,如果没有继续比较下一个,直到找到第一个比基准值小的值才交换。找到这个值之后,又从前往后开始比较,如果有比基准值大的,交换位置,如果没有继续比较下一个,直到找到第一个比基准值大的值才交换。直到从前往后的比较索引>从后往前比较的索引,结束第一次循环,此时,对于基准值来说,左右两边就是有序的了。
接着分别比较左右两边的序列,重复上述的循环。
时间复杂度最好:o(n) 最坏 :O(n^2)退换成冒泡排序 空间复杂度:O(1), 不稳定
代码实现(递归):
public static void main(String[] args) {
int[] arr = new int[20];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random () * 100);
}
quickSort (arr, 0, arr.length - 1);
System.out.println (Arrays.toString (arr));
}
private static void quickSort(int[] arr, int i, int j) {
if (i > j)
return;
int l = partation (arr, i, j); // left == right
quickSort (arr, i, l - 1);
quickSort (arr, l + 1, j);
}
/**
* 返回值l就是基准数应该放置的位置
* @param arr
* @param l
* @param r
* @return
*/
private static int partation(int[] arr, int l, int r) { // O(log2n)
int val = arr[l];
while (l < r) {
// 1. 从r开始往l的方向找第一个小于val的数字
while (l < r && arr[r] > val) {
r--;
}
// 2. 把第一个小于val的元素值写入l里面,并且l++
if (l < r) {
arr[l++] = arr[r];
}
// 3. 从l往r的方向找第一个大于val的数字
while (l < r && arr[l] < val) {
l++;
}
// 4. 把第一个大于val的元素值写入r里面,并且r--
if (l < r) {
arr[r--] = arr[l];
}
}
arr[l] = val;
return l;
}
}
在海量数据中,找值第k小的,或者值第k大的元素使用快排分割函数,代码如下:
//@Test的使用 是该方法可以不用main方法调用就可以测试出运行结果,是一种测试方法。在一个代码中只能写一个main方法,但是可以写多个@Test,有几个@Test注解,就运行几个test方法。
@Test
public void test05(){
int[] arr = new int[20];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int)(Math.random()*1000);
}
int data = select_no_k_value(arr, 0, arr.length-1, 5);//找出第五小的元素data
System.out.println("data: " + data);
//这里调用上面写的quickSort,不在书写
quickSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
/**
* 在arr中,找第k小的元素
* @param arr
* @param i
* @param j
* @param k
* @return
*/
private int select_no_k_value(int[] arr, int i, int j, int k) {
int no = partation(arr, i, j); // arr[no]
if(no > k-1){
return select_no_k_value(arr, i, no-1, k);
} else if(no < k-1){
return select_no_k_value(arr, no+1, j, k);
} else {
return arr[no];
}
}
}
快排是越排元素越有序,效率也就逐渐降低,因此快排有一些优化措施:
在一组基本有序的序列中,进行排序操作,插入排序效率是最高的,当数据范围小到一定程度的时候,采用插入排序代替快排。
5.希尔排序
希尔排序也成为“缩小增量排序”,其基本原理是,现将待排序的数组元素分成多个子序列,使得每个子序列的元素个数相对较少,然后对各个子序列分别进行直接插入排序,待整个待排序列“基本有序”后,最后在对所有元素进行一次直接插入排序。因此,我们要采用跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。希尔排序是对直接插入排序算法的优化和升级。
时间复杂度最好是O(n),最坏是O(n^2) 空间复杂度O(1) 不稳定
代码实现:
在这里插入代码片
6.归并排序
基本思想:是外部排序
归并排序是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列
每个递归过程涉及三个步骤
第一, 分解: 把待排序的 n 个元素的序列分解成两个子序列, 每个子序列包括 n/2 个元素.
第二, 治理: 对每个子序列分别调用归并排序MergeSort, 进行递归操作
第三, 合并: 合并两个排好序的子序列,生成排序结果.
时间复杂度:最好和最坏都是O(nlog2n) 空间复杂度为O(n) 稳定
代码实现
public static void main(String[] args) {
int[] arr = new int[20];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int)(Math.random()*100);
}
mergeSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
private static void mergeSort(int[] arr, int i, int j) {
if(i < j)
{
int mid = (i+j)/2;
/**
* 以下的操作,先进行数组划分,直到划分为单个元素以后,逐级向上回溯
* 的时候,进行合并操作
*/
mergeSort(arr, i, mid);
mergeSort(arr, mid+1, j);
//左右归并
merge(arr, i, j); // 合并两个有序的序列
}
}
private static void merge(int[] arr, int low, int high) {
int[] tmp = new int[high-low+1];
int mid = (low+high)/2; // i-mid mid+1-j
int i=low; // [i, mid]
int j=mid+1; // [mid+1, high]
int idx=0;
// 把较小的数先移到新数组中
while(i <= mid && j <= high){
if(arr[i] > arr[j]){
tmp[idx++] = arr[j++];
} else {
tmp[idx++] = arr[i++];
}
}
// 把左边剩余的数移入数组
while(i <= mid){
tmp[idx++] = arr[i++];
}
// 把右边边剩余的数移入数组
while(j <= high){
tmp[idx++] = arr[j++];
}
// 把tmp里面合并的有序段再写回arr的[low,high]
for(int k=low; k<=high; ++k){
arr[k] = tmp[k-low];
}
}
}
7.堆排序
堆排序是一种树形选择排序方法,它的特点是:在排序的过程中,将array[0,…,n-1]看成是一颗完全二叉树的顺序存储结构,利用完全二叉树中双亲节点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(最小)的元素。
- 若array[0,…,n-1]表示一颗完全二叉树的顺序存储模式,则双亲节点指针和孩子结点指针之间的内在关系如下:
任意一节点指针 i:父节点:i==0 ? null : (i-1)/2
左孩子:2i + 1
右孩子:2i + 2 - 堆的定义:n个关键字序列array[0,…,n-1],当且仅当满足下列要求:(0 <= i <= (n-1)/2)
① array[i] <= array[2i + 1] 且 array[i] <= array[2i + 2]; 称为小根堆;
② array[i] >= array[2i + 1] 且 array[i] >= array[2i + 2]; 称为大根堆; - 建立大根堆:
n个节点的完全二叉树array[0,…,n-1],最后一个节点n-1是第(n-1-1)/2个节点的孩子。对第(n-1-1)/2个节点为根的子树调整,使该子树称为堆。
对于大根堆,调整方法为:若【根节点的关键字】小于【左右子女中关键字较大者】,则交换。
之后向前依次对各节点((n-2)/2 - 1)~ 0为根的子树进行调整,看该节点值是否大于其左右子节点的值,若不是,将左右子节点中较大值与之交换,交换后可能会破坏下一级堆,于是继续采用上述方法构建下一级的堆,直到以该节点为根的子树构成堆为止。
反复利用上述调整堆的方法建堆,直到根节点。
4.堆排序:(大根堆)
①将存放在array[0,…,n-1]中的n个元素建成初始堆;
②将堆顶元素与堆底元素进行交换,则序列的最大值即已放到正确的位置;
③但此时堆被破坏,将堆顶元素向下调整使其继续保持大根堆的性质,再重复第②③步,直到堆中仅剩下一个元素为止。
空间复杂度:O(1); 稳定性:不稳定
时间复杂度:建堆:O(n),每次调整O(log2 n),故最好、最坏、平均情况下:o(n*logn);
代码实现:
public static void main(String[] args) {
int[] arr = new int[10];
Random rd = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = rd.nextInt(100);
}
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void heapSort(int[] arr) {
int n = arr.length-1;
// 从第一个非叶子节点开始,把大值往父节点调整
for(int i=(n-1)/2; i>=0; --i){
adjust(arr, i, arr.length);
}
for(int i=n; i>=0; --i){
//0 <=> i 它们的值进行交换
int tmp = arr[0];
arr[0] = arr[i];
arr[i] = tmp;
//再继续进行堆的调整 adjust
adjust(arr, 0, i);
}
}
/**
* 堆的调整函数,把每一个节点,和其左右孩子节点的最大值放到当前节点处
* @param arr
* @param i
* @param length
*/
private static void adjust(int[] arr, int i, int length) {
int val = arr[i];
for(int j=2*i+1; j<length; j=2*j+1){
// 先用j标识值最大的孩子
if(j+1 < length && arr[j+1] > arr[j]){
j++;
}
if(arr[j] > val){
arr[i] = arr[j];
i = j;
} else {
break;
}
}
arr[i] = val;
}
}