目录
前言
八大排序可以算是《数据结构与算法》这门课的作为经典的一环,不管是期末考试还是面试都有它的存在。面试中,手写还是说思路都屡见不鲜了。面试中还有一种问法是“快排的第二次”、“归并的第三次”,这就要你不但要求你会写,而且脑中要有一个排序的过程。另外分析时间空间复杂度也是一定会被考察的,难一点还会问最优最差的复杂度,因此思考算法的同时,还要想一些例子。本文会将分析排序的大致思想、复杂度,部分算法将同时给出链表的实现。废话不多说,直接开始吧。
力扣排序数组
冒泡排序
冒泡排序的“冒泡”可以看作一个数值移动的过程。每回合都从头开始比较,通过两两交换,最终将一个当前回合的极值固定到尾部,下一回合只需要对剩下的数做上述过程即可。
数组的冒泡排序
public int[] sortArray_bubble(int[] nums) {
for (int i = 0; i < nums.length-1; i++) {
//当前轮有i个数被固定
boolean b = false;
for (int j = 0; j+1 < nums.length-i; j++) {
//末尾有i个数被固定
if(nums[j+1]<nums[j]){
swap(j,j+1,nums);
b=true;
}
}
if(!b)break;
}
return nums;
}
做两点说明:数组交换swap和链表交换swapListNode都是简单的值交换,不再给出具体实现。同时方法内部已经做了优化(不会交换相同的值)
代码做了优化:如果一次冒泡没有进行任何交换,那么便认为整个数组有序,退出。
几个注意点:
【1】外循环控制的“当前回合有几个值被固定了/上浮完毕了”,我们在内循环减去这个值,没有比较的必要了。其中减一的意思是“最后一轮只有一个元素未固定,那么没有比较的必要了,直接退出即可”
【2】内循环如果写作 j < len - i -1 我认为可读性有点差,当你初学时死活不懂课本上写的什么意思。其实就是一个越界检查罢了。初学者容易写成 j = i ,就是没有理解内循环的意义罢了。内循环就是一个从头开始,和相邻元素两两比较的过程,唯一使用到外循环变量的就是知道“哪些没必要比较”
【3】没有使用额外空间,因此空间复杂度O(1),最好的情况一次内循环发现没有任何交换直接退出,因此O(N),最坏情况从头比到尾O(N^2)
链表的冒泡排序
冒泡排序说白了:外层控制终点,内层从起点开始两两比较,用链表实现一个道理,使用一个指针标记结尾,一个指针标记开头。外循环修改尾指针,内循环每回合都重置头指针。
public ListNode bubbleSort(ListNode head){
if(head == null || head.next == null)
return head;
ListNode tail = null;
ListNode cur = head;
while(cur.next != tail){
boolean flag=false;//没有交换
while(cur.next != tail){
if(cur.val > cur.next.val){
swapListNode(cur,cur.next);
flag=true;
}
cur = cur.next;
}
if(!flag)break;
tail = cur; //“气泡固定”通过尾指针移动减少范围来表现
cur = head; //从头再来
}
return head;
}
选择排序
选择排序选择什么?常规的升序降序选择的是机制,当然算法就是一个思想,不同的应用选择的值也不一样。选择排序的思想:每次选择一个极值,然后存入目标集合,直到选择完毕所有元素。
数组的选择排序
以升序为例:
private void selectSort(int[] arr)
{
//最终一轮剩下的就是非最小值,因此 arr.length-1
for (int i = 0; i < arr.length-1; i++) {
int minIndex = selectMin(arr, i, arr.length);
if(minIndex!=i){
//当前索引对应的不是最小值
swap(i,minIndex,arr);//交换
}
}
}
选择的范围是不断缩小的,依次选出最小值、次最小值、直到选择完毕所有的值
private int selectMin(int[] arr, int start, int end) {
int min=Integer.MAX_VALUE;
int minIndex=0;
for (int i = start; i < end; i++) {
if(arr[i]<min){
min=arr[i];
minIndex=i;
}
}
return minIndex;
}
选择排序的复杂度总是O(n^2),因为即使给出的数组是有序的,选择时也是感觉不到的,只知道“当前我要选择的是当前范围的最值”。
选择排序算法是不稳定的,如552,第一次选择完毕变成255,其中两个5的相对位置被改变了。
链表的选择排序
如果把链表看出有序与无序两个部分,一个指针指向无序链表的头结点,一个指针用于扫描无序链表做选择,扫描的过程中两个指针指向的节点的值将做交换。扫描完毕后,无序头结点便是当前无序链表中最小的,加入有序链表尾部,更新无序链表头部…指定全部节点被加入有序链表
其实也可以使用一个指针保存最小节点,扫描完毕后进行一次节点的交换,避免了过程中重复的值交换。总之实现方法很多。
public ListNode sortList(ListNode head) {
if(head == null){
return head;
}
ListNode curNode = head;
while(curNode!=null){
ListNode nextNode = curNode.next;
while(nextNode!=null){
//和更小的节点交换值
if(curNode.val > nextNode.val){
swapListNode(curNode,nextNode);
}
nextNode = nextNode.next;
}
curNode = curNode.next;
}
return head;
}
插入排序
之前提到了,可以将序列看作两部分:有序的和无序的。选择排序每次从无序序列中选择一个极值。而插入排序不对无序序列进行选择(扫描),而是对有序序列进行选择(插入)。插入排序每次取无序序列首元素,然后再从有序序列中选择一个合适的位置,进行插入即可。
由于扫描的是有序序列,因此可以“感知”扫描停止的时机,不像扫描无序序列那样,必须全部扫描,生怕漏了那个数。
数组的插入排序
插入排序可以分为直接插入排序和二分插入排序,后者更快,对有序序列进行二分插入,而不是从一端向令一端遍历。(但是仍然无法避免数组数据的整体移位)
直接插入排序:
public void insertSort(int[] arr){
for (int i = 0; i+1 < arr.length; i++) {
//定义待插入的数
int insertVal = arr[i+1];
//给insertVal 找到插入的位置
int indexPreIndex;
for( indexPreIndex= i;indexPreIndex>=0 && insertVal<arr[indexPreIndex];indexPreIndex--){
arr[indexPreIndex+1]=arr[indexPreIndex];//整体后移
}
arr[indexPreIndex+1]=insertVal;//insertVal>=arr[indexPreIndex],要插入到后面的位置保证有序
}
}
二分插入排序:
public void insertSort2(int