常用排序算法总结(数组+链表)Java

一、简介

最近在复习算法和数据结构,复习到排序和查找,在这里对常用的几种排序算法做一个总结

先放一张图,这张图包括了十种经典排序算法的一些特点和复杂度,接下来就按照图中的顺序来进行具体的分析
在这里插入图片描述

二、具体分析及代码

说明:本文中的排序目标均为升序
*定义swap方法:

    private void swap(int[]nums, int i, int j) {
    
    
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

1.冒泡排序

冒泡排序是最简单的排序方法之一,原理十分直接,对于数组中的每一个数,不断将它与右侧的数进行比较,如果右侧的数大于它就将它和右侧的数交换,直到没有比他大的数为止,这个过程类似气泡在水中上浮的过程,故得名冒泡排序

冒泡排序的代码如下

 public int[] bubbleSort(int[] nums) {
    
    
        for(int i = 0; i < nums.length; i++) {
    
    
        	boolean flag = false;
            for(int j = 0; j < nums.length - i - 1; j++) {
    
    
                if(nums[j] > nums[j + 1]) {
    
    
                    swap(nums, j, j + 1);
                    flag = true;
                }
            }
          	if(!flag) {
    
    //若flag为false说明原数组中任意相邻的两个数都是有序的,因此整个数组已经有序,所以不需要进行接下来的比较直接返回true
          		return true;
          	}
        }
        return nums;
    }

接下来分析代码的复杂度。很明显能看出,当待排序数组为降序时,最内层代码最多需要执行(1 +…+ n - 1) 次,当待排序数组为升序时,内层代码需要执行n次,。平均时间复杂度为O(n^2),冒泡排序不需要额外的空间,因此空间复杂度为O(1)。当相邻两个数相等时不进行交换,因此冒泡排序是稳定排序。

2.选择排序

选择排序,顾名思义,每次循环中选择一个最大或者最小的数将它放到数组的一侧,假设每次选择一个最小的数,首先将该数放到最左侧,然后从该数的下一个位置开始遍历,重复这一过程,每个被放到数组左侧的数都是剩余数组中的最小值,该数的左侧都是比它小的数且已经有序。遍历一遍之后原数组就成为有序的了,选择排序的具体过程可以参考下面这张图
在这里插入图片描述

参考代码如下:

public int[] choseSort(int[] nums) {
    
    
        for(int i = 0; i < nums.length; i++) {
    
    
            int min = i;
            for(int j = i + 1; j <nums.length; j++) {
    
    
                if(nums[min] > nums[j]) {
    
    
                    min = j;
                }
            }
            swap(nums, i, min);//i所在的位置是有序子数组的末尾
        }
        return nums;
    }

下面分析复杂度。不难看出,无论什么情况,内层代码都要执行(1 + …+ n - 1) = n ^ 2 / 2 次,因此时间复杂度为O(n^2)。选择排序不需要额外空间,因此空间复杂度为O(1)。选择排序中原本排在前面的数字插入时可能被移到后面(如[9,3,6,9,1],第一次扫描后变为[1,3,6,9,9],原本在前面的9跑到了后面)。因此选择排序是不稳定排序。

*当排序对象不是数组而是单链表时,选择排序依然适用,思路同数组的类似,这里设立一个辅助头指针指向原链表的头部方便插入节点

代码如下:

    public ListNode linkedChoseSort(ListNode head) {
    
    
        ListNode headPre = new ListNode();
        headPre.next = head;//辅助头节点
        
        ListNode cur = headPre;//计数指针
        while(cur.next != null) {
    
    
            ListNode min = cur.next;//初始化最小值为cur的下一位
            ListNode flag = cur.next;
            ListNode minPre = flag;//因为插入节点需要修改该节点前一个节点的next,这里用一个指针标记前一个节点
            while(flag.next != null) {
    
    
                if(flag.next.val < min.val) {
    
    //如果当前节点的下一个节点值小于min,则更新
                    min = flag.next;
                    minPre = flag;
                }
                flag = flag.next;
            }
            if(min != cur.next) {
    
    
                ListNode target = cur.next;//targer为左侧要进行交换的节点
                if(minPre == target) {
    
    //如果target是min的前置节点,直接交换
                    minPre.next = min.next;
                    cur.next = min;
                    min.next = minPre;
                } else {
    
    
                    ListNode afterNext = target.next;
                    minPre.next = target;
                    cur.next = min;
                    target.next = min.next;
                    min.next = afterNext;
                }
            }
            cur = cur.next;
        }
        return headPre.next;
    }

3.插入排序

插入排序的总体思想是:首先认为第一个数是有序的,从第二个数开始,对于数组中的一个数m,不断向左寻找直到找到一个比m小的数n,然后将m插入到n之后,若直到数组头部还没找到就将该数放在数组头部,实现局部有序,当整个数组被遍历完之后就成为了一个有序的数组。
插入排序的具体过程可以参考下图:
在这里插入图片描述

插入排序的具体代码如下:

public int[] insertSort(int[] nums) {
    
    
        if(nums.length < 2) {
    
    
            return nums;
        }
        for(int i = 1; i < nums.length; i++) {
    
    
            int j = i - 1;
            while(j >= 0 && nums[j] > nums[i]) {
    
    //i - 1;若碰到相等的数就停止,保证了稳定性
                j--;
            }
            int temp = nums[i];
            for(int k = i; k > j + 1; k--) {
    
    //i - j - 1;
                nums[k] = nums[k - 1];
            }
            nums[j + 1] = temp;
        }
        return nums;
    }

复杂度分析:
由代码可以看出,最坏情况(数组为逆序时)对于每一个i,内层循环最多执行2 *(i - 1)次,故最坏情况时间复杂度为O(n^2 )最好情况(数组为升序时)只需进行n次比较,此时时间复杂度为O(n),平均时间复杂度为O(n^2),插入排序不需要用到额外的空间,故空间复杂度为O(1)。插入排序向左搜索时遇到相等的数就停止,故原本在前的数排序后还是在前(如[3,9,5,1,9,2]->[3,5,9,1,9,2]->[1,3,5,9,9,2]),故插入排序是稳定的排序。

4.归并排序

归并排序中使用到了分治的思想,当一个数组中只有一个元素时,它是有序的,若一个数组有两个以上元素,可以拆成两个子数组,若每个子数组都有序,则将它们合并之后的数组也是有序的,这就是归并排序的核心。
下面是归并排序的演示图:
在这里插入图片描述

归并排序的递归代码如下:

    public int[] mergeSort(int[] nums) {
    
    
        if(nums.length < 2) {
    
    
            return nums;
        }
        int flag = nums.length / 2;
        int[] a = Arrays.copyOfRange(nums, 0, flag);
        int[] b = Arrays.copyOfRange(nums, flag, nums.length);
        return merge(mergeSort(a), mergeSort(b));
    }
    
    private int[] merge(int[] a, int[] b) {
    
    
        int[] res = new int[a.length + b.length];
        int i = 0;
        int m = 0;
        int n = 0;
        while(i < a.length + b.length) {
    
    
            if(m < a.length && n < b.length) {
    
    
                if(a[m] <= b[n]) {
    
    
                    res[i] = a[m++];
                } else {
    
    
                    res[i] = b[n++];
                }
            } else if(m >= a.length) {
    
    
                res[i] = b[n++];
            } else if(n >= b.length) {
    
    
                res[i] = a[m++];
            }
            i++;
        }
        return res;
    }

非递归代码如下:

   public static int[] mergeSort(int[] nums) {
    
    
       int n = nums.length;
       for (int i = 1; i < n; i *= 2) {
    
    
           int left = 0;
           int mid = left + i - 1;
           int right = mid + i;
           while (right < n) {
    
    
               merge(nums, left, mid, right);
               left = right + 1;
               mid = left + i - 1;
               right = mid + i;
           }
           if (left < n && mid < n) {
    
    
               merge(nums, left, mid, n - 1);
           }
       }
       return nums;
   }
private void merge(int[] nums, int left, int mid, int right) {
    
    
  
        int[] a = new int[right - left + 1];
        int i = left;
        int j = mid + 1;
        int k = 0;
        while (i <= mid && j <= right) {
    
    
            if (nums[i] < nums[j]) {
    
    
                a[k++] = nums[i++];
            } else {
    
    
                a[k++] = nums[j++];
            }
        }
        while(i <= mid) a[k++] = nums[i++];
        while(j <= right) a[k++] = nums[j++];
        // 把临时数组复制到原数组
        for (i = 0; i < k; i++) {
    
    
            nums[left++] = a[i];
        }
    }

*当目标序列为链表时的归并排序如下:

	private void insert(ListNode l1, ListNode l2) {
    
    
		if(l1 == null || l2 == null) {
    
    
            return;
        }
		l1.next = l2;
	
    }

    public ListNode linkedMerge(ListNode l1, ListNode l2) {
    
    
        ListNode p1 = l1;
        ListNode p2 = l2;
        ListNode head = new ListNode();
        ListNode p = head;
        while(l1 != null && l2 != null) {
    
    
            if(l1.val < l2.val) {
    
    
                insert(p, l1);
                p = p.next;
                l1 = l1.next;
            } else {
    
    
                insert(p, l2);
                p = p.next;
                l2 = l2.next;
            }
        }
        if(l1 == null) {
    
    
            insert(p, l2);
        } else {
    
    
            insert(p, l1);
        }
        
        return head.next;
    }

    public ListNode linkedMergeSort(ListNode head) {
    
    
        if(head == null || head.next == null) {
    
    
            return head;
        }
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null && fast.next.next != null) {
    
    //用快慢指针找到链表中点
            fast = fast.next.next;
            slow = slow.next;
        }
        ListNode rightHead = slow.next;
        slow.next = null;//将链表从中点处断开,然后进行递归sort后merge
        return linkedMerge(linkedMergeSort(head), linkedMergeSort(rightHead));
    }

归并排序的时间复杂度为O(nlogn)。当对象为数组时,在排序过程中需要用到额外的n份空间,栈空间使用带来的复杂度为O(logn),故总的空间复杂度为O(n)。当排序对象为链表时,不需要额外空间,此时时间复杂度为O(1)。归并排序在合并时保留了原序列顺序,故归并排序是稳定的排序。

5.快速排序

快速排序的核心思想是先找一个数作为基准,然后把数组中比它小的数都放到该数左边,比它大的数都放到该数右边,操作结束后该数的位置就是正确位置,接下来递归地对该数左侧和右侧的数进行操作。
快速排序的过程可以参考下图:
在这里插入图片描述

代码如下:

 public int[] quickSort(int[] nums, int left, int right) {
    
    
        if(left >= right) {
    
    
            return nums;
        }
        int index = getIndex(nums, left, right);//获取基准数在数组中的正确下标
        quickSort(nums, left, index - 1);//递归地对基准数左右侧进行排序
        quickSort(nums, index + 1, right);
        return nums;
    }

    private int getIndex(int[] nums, int left, int right) {
    
    
        int l = left; 
        int r = right;
        int flag = nums[left];//以第一个数为基准

        while(l < r) {
    
    
            while(l < r && nums[r] >= flag) {
    
    
                r--;
            }
            nums[l] = nums[r];
            while(l < r && nums[l] <= flag) {
    
    
                l++;
            }
            nums[r] = nums[l];
        }
        nums[l] = flag;
        return l; 
    }

快速排序在原数组有序的情况下复杂度最高,为O(n^2), 平均的时间复杂度是O(nlogn)。快速排序不需要额外的空间,主要空间复杂度来源于递归时栈空间的使用,平均复杂度为O(logn),由于快速排序遇到相等的数仍然交换位置,(例如:[3,2,1,3,4],选取第一个3为基准,则在排序中若取等号会将第二个3移到第一个的左侧,若不取则会交换第二个3和第一个3的位置)故快速排序不是稳定排序。

6.希尔排序

希尔排序是插入排序的一个变种,插入排序一次只能将数据移动一位,同时插入排序在数据较为有序的时候复杂度较好,希尔排序通过将数据进行分组,每次使得组内数据有序,并不断减少组数,最好剩下一组时,整个数组就有序了。
具体代码如下:

public int[] shellSort(int[] nums) {
    
    
        for(int gap = nums.length / 2; gap > 0; gap /= 2){
    
    
           //从第gap个元素,逐个对其所在组进行直接插入排序操作
           for(int i = gap; i < nums.length; i++){
    
    
               int j = i;
               while(j - gap >= 0 && nums[j] < nums[j - gap]){
    
    
                   swap(nums, j, j - gap);
                   j -= gap;
               }
           }
       }
       return nums;
    }

希尔排序的时间复杂度为O(nlogn),空间复杂度为O(1),由代码可以看出,希尔排序是由多次插入排序组合而成,虽然每组内的插入排序是稳定的,但对于整个数组来说元素的前后顺序可能发生改变,故希尔排序不是稳定的排序。

7.计数排序

计数排序的原理是:先遍历找到原数组中最大值max和最小值min,然后设置偏移量flag为max - min,接着用一个大小为flag的辅助数组保存原数组中数出现的次数,该数组下标加上flag就是原数组中的数。
然后从头开始查找辅助数组,若当前位置值不为0,则输出辅助数组当前下标加上flag的值,同时将当前位置值减1,若当前值为0则前进一步,直到数组末尾。
具体代码如下:

  public int[] countSort(int[] nums) {
    
    
        if(nums.length < 2) {
    
    
            return nums;
        }
        int max = nums[0];
        int min = max;
        for(int i:nums) {
    
    
            if(i > max) {
    
    
                max = i;
            }
            if(i < min) {
    
    
                min = i;
            }
        }
        int len = max - min + 1;
        int[] count = new int[len];
        for(int i:nums) {
    
    
            count[i - min]++;
        }
        int flag = 0;
        //不稳定的做法,直接按照顺序输出
        //for(int j = 0;j < count.length; j++) {
    
    
            //while(count[j] > 0) {
    
    
               // nums[flag++] = j + min;
               // count[j]--;
            //}
        //}
        //return nums;
        
		//稳定的做法,从原数组尾端扫描,依次插入新数组
		int[] res = new int[nums.length];
        for(int i = 1; i < count.length; i++) {
    
     
            count[i] += count[i - 1];
        }
		for(int j = nums.length - 1; j >=0; j--) {
    
    
			int index = nums[j] - min;
			if(count[index] > 0) {
    
    
				res[count[index] - 1] = nums[j];
				count[index]--;
			}
			
		}
		return res;
    }    

计数排序的时间复杂度是O(n+k),由于用到了额外长度为n的数组和k = max-min的辅助空间,故空间复杂度是O(n+k)。是稳定排序。如果直接用原数组输出,则空间复杂度为O(k),但此时排序是不稳定的。

8.堆排序

时间原因先写到这,堆排序、桶排序和基数排序等面试完再补充吧

9.桶排序

10.基数排序

猜你喜欢

转载自blog.csdn.net/weixin_42970016/article/details/113096262
今日推荐