排序算法也是面试中常常提及的内容,问的最多的应该是快速排序、堆排序、归并排序。这些排序算法很基础,很可能知道其思想,但在写代码时细节上出错。因此本文的书写主要参考了十大经典排序算法动画与解析,看我就够了!(配代码完全版),对基于比较的排序算法:冒泡排序、插入排序、选择排序以及归并排序、快速排序和堆排序重新自己实现了一遍。上文中给出的算法动画有助于排序算法的理解。以下算法全部基于数组原地排序。
import java.util.Arrays;
public class Sort {
public static void main(String[] args){
int[] array = {8,7,6,5,4,3,2,1};
//bubbleSort(array);
//selectionSort(array);
//insertionSort(array);
//mergeSort(array,0,array.length - 1);
//quickSort(array,0,array.length - 1);
//heapSort(array);
System.out.println(Arrays.toString(array));
}
// 冒泡排序 稳定 O(n2)
// 相邻的两两比较 每次将最小值冒泡到最顶部
public static void bubbleSort(int[] array) {
for(int i = 0;i < array.length - 1;i++) {
for(int j = array.length - 1; j > i; j--) {
if(array[j] < array[j - 1]) {
int tmp = array[j];
array[j] = array[j-1];
array[j-1] = tmp;
}
}
}
}
// 插入排序 稳定 O(n2)
// 前面的数组是有序的,后面为待排数组,在有序数组中找到待排数的插入位置
public static void insertionSort(int[] array) {
for (int i = 1; i < array.length; i++) {
// 从后向前依次比较,找到插入位置后结束
for (int j = i; j > 0; j--) {
if (array[j - 1] > array[j]) {
int tmp = array[j];
array[j] = array[j - 1];
array[j - 1] = tmp;
}
else
break;
}
}
}
// 选择排序 不稳定 O(n2)
// 找到数组中的最小值,将数组开头的值和最小值交换
public static void selectionSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < array.length; j++) {
if (array[j] < array[minIndex]){
minIndex = j;
}
}
int tmp = array[i];
array[i] = array[minIndex];
array[minIndex] = tmp;
}
}
// 归并排序 稳定 O(nlogn)
// 递归 将两个有序的数组合成一个有序的数组
public static void mergeSort(int[] array,int left,int right) {
if (left == right) {
return ;
}
int mid = left + (right - left) / 2;
mergeSort(array,left,mid);
mergeSort(array,mid + 1,right);
merge(array,left,mid,right);
}
public static void merge(int[] array,int left,int mid,int right) {
int[] tmp = new int[right - left + 1];
int pointerLeft = left;
int pointerRight = mid + 1;
for (int i = 0;i < tmp.length; i++) {
if (pointerLeft > mid) {
tmp[i] = array[pointerRight];
pointerRight++;
} else if (pointerRight > right) {
tmp[i] = array[pointerLeft];
pointerLeft++;
} else {
if (array[pointerLeft] <= array[pointerRight]) {
tmp[i] = pointerLeft;
pointerLeft++;
} else {
tmp[i] = array[pointerRight];
pointerRight++;
}
}
}
for (int i = 0;i < tmp.length; i++) {
array[left + i] = tmp[i];
}
}
// 快速排序 不稳定 O(nlogn)
// 找一个基准值(可以为数组第一个),从右侧找比基准值小的填左侧的坑,从左边找比基准值大的填右边的坑,直到左右指针相遇,将基准值填入
// 此时,基准值左侧值全小于基准值,右侧值全大于基准值,再对基准值左右两侧数组进行快速排序
public static void quickSort(int[] array, int left, int right) {
if (left >= right) {
return;
}
int flag = array[left];
int pointerLeft = left;
int pointerRight = right;
boolean positionLeft = true; //填flag左侧的坑
while (pointerLeft != pointerRight) {
if (positionLeft) {
if (array[pointerRight] < flag) {
// 左侧的坑填上
array[pointerLeft] = array[pointerRight];
positionLeft = false;
} else {
pointerRight--; // 继续寻找
}
} else {
if (array[pointerLeft] > flag) {
// 右侧的坑填上
array[pointerRight] = array[pointerLeft];
positionLeft = true;
} else {
pointerLeft++; //继续寻找
}
}
}
array[pointerLeft] = flag;
quickSort(array,left,pointerLeft - 1);
quickSort(array,pointerLeft + 1,right);
}
// 堆排序 不稳定 O(nlogn)
// 最大堆实现升序排序(最大堆:父节点大于儿子节点的值)
// 数组索引即为节点编号,满足left = root*2 + 1; right = root*2 + 2
// pushDown 只有根节点不满足最大堆条件,将根节点一直下推直至整理成最大堆
// initHeap 初始建堆 从最下方的子树开始一直向上进行下推建堆
public static void heapSort(int[] array) {
int len = array.length - 1;
initHeap(array);
for (int i = 0;i < len; i++) {
int tmp = array[0];
array[0] = array[len-i];
array[len-i] = tmp;
pushDown(array,0,len-i-1);
}
}
public static void initHeap(int[] array) {
int len = array.length - 1;
for (int i = (len - 1) / 2;i>=0;i--) {
pushDown(array,i,len);
}
}
public static void pushDown(int[] array, int root, int len) {
int left = root * 2 + 1;
int right = root * 2 + 2;
int largest = root;
if (right <= len && array[right] > array[largest]) {
largest = right;
}
if (left <= len && array[left] > array[largest]) {
largest = left;
}
//根节点被下推了
if (largest != root) {
int tmp = array[root];
array[root] = array[largest];
array[largest] = tmp;
pushDown(array, largest, len);
}
}
}
给出一个表格总结排序算法的时空复杂度和稳定情况
排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 是否稳定 |
冒泡排序 | ![]() | ![]() | ![]() | 是 |
插入排序 | ![]() | ![]() | ![]() | 是 |
选择排序 | ![]() | ![]() | ![]() | 否 |
归并排序 | ![]() | ![]() | ![]() | 是 |
快速排序 | ![]() | ![]() | ![]() | 否 |
堆排序 | ![]() | ![]() | 递归 ![]() | 否 |
非递归 ![]() |
- 排序算法的稳定性:排序前后相同元素的相对位置不变,则称排序算法是稳定的;否则排序算法是不稳定的。
- 归并排序需要使用辅助数组来合并两个数组,空间复杂度为
- 快速排序递归方法使用函数栈空间,空间复杂度平均为
,最差为
;非递归方法仍需使用自定义栈,空间复杂度为
- 堆排序递归方法使用函数栈空间,空间复杂度为
;非递归方法空间复杂度为
LeetCode排序算法总结
LeetCode上的排序算法有一些是先将数据按一定规则排成有序数据,再对这些数据进行处理,如56。也有对排序算法的实现,如147在链表上实现插入排序;184排序链表,在O(nlogn)时间复杂度下,其实是链表的归并排序。
56.合并区间
给出一个区间的集合,请合并所有重叠的区间。
输入: [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
输入: [[1,4],[4,5]]
输出: [[1,5]]
解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
将区间按起始端点升序排序,之后按这个顺序两两检查区间是否能合并。本题的思路比较简单,重点在于使用Java的比较器Comparator自定义排序。对于数组可使用Array.sort(),自定义比较器Comparator实现compare函数,返回值定义如下:
这里o1表示位于前面的对象,o2表示后面的对象
- 返回负数,表示不需要交换01和02的位置
- 返回正数,表示需要交换01和02的位置
class Solution {
public int[][] merge(int[][] intervals) {
List<int[]> ret = new ArrayList<>();
if (intervals.length < 2) {
return intervals;
}
Arrays.sort(intervals,new Comparator<int[]>(){
public int compare(int[] o1, int[] o2) {
return o1[0] - o2[0];
}
});
for(int i = 0; i < intervals.length - 1; i++) {
if (intervals[i][1] >= intervals[i+1][0]) {
intervals[i+1][0] = intervals[i][0];
intervals[i+1][1] = Math.max(intervals[i][1],intervals[i+1][1]);
} else {
ret.add(intervals[i]);
}
if (i == intervals.length - 2) {
ret.add(intervals[intervals.length - 1]);
}
}
return ret.toArray(new int[0][]);
}
}
147.对链表进行插入排序
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode insertionSortList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode blank = new ListNode(0);
blank.next = head;
while (head.next != null) {
ListNode cur = head.next;
ListNode p = blank;
while (p != head) {
if (p.next.val > cur.val) { //插入
head.next = cur.next;
cur.next = p.next;
p.next = cur;
break;
} else {
p = p.next; //找位置
}
}
if (p == head) {
head = head.next; //插到最后只需要改变排序完的链表尾指针的位置
}
}
return blank.next;
}
}
148.排序链表
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
输入: 4->2->1->3
输出: 1->2->3->4
输入: -1->5->3->4->0
输出: -1->0->3->4->5
给出归并排序和快速排序两种解法。
归并排序,使用快慢指针找到链表中心点,两边递归后再merge。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
if(head == null || head.next == null) {
return head;
}
//归并排序
ListNode slow = head;
ListNode fast = head.next;
while(fast!=null && fast.next!=null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode right = sortList(slow.next);
slow.next = null;
ListNode left = sortList(head);
// merge
ListNode blank = new ListNode(0);
ListNode p = blank;
while(left!=null||right!=null) {
if(left==null) {
p.next = right;
break;
} else if(right == null) {
p.next = left;
break;
} else {
if(left.val < right.val) {
p.next = left;
p = p.next;
left = left.next;
} else {
p.next = right;
p = p.next;
right = right.next;
}
}
}
return blank.next;
}
}
快速排序。实现了一个三路快排,将链表中小于基准值的节点放在一个链表中,大于的放在一个链表中,等于的放在一个链表中,小于和大于的部分继续归并排序,然后将这三个链表连起来。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
if(head==null || head.next==null) {
return head;
}
int flag = head.val;
ListNode leftblank = new ListNode(0);
ListNode leftp = leftblank;
ListNode rightblank = new ListNode(0);
ListNode rightp = rightblank;
ListNode middlep = head;
ListNode p = head.next;
while(p!=null) {
if(p.val<flag) {
leftp.next = p;
leftp = leftp.next;
} else if(p.val > flag) {
rightp.next = p;
rightp = rightp.next;
} else {
middlep.next = p;
middlep = middlep.next;
}
p = p.next;
}
leftp.next = null;
rightp.next = null;
ListNode left = sortList(leftblank.next);
ListNode right = sortList(rightblank.next);
p = left;
while(p!=null && p.next!=null) {
p = p.next;
}
if(p==null) {
left = head;
} else {
p.next = head;
}
middlep.next = right;
return left;
}
}
判断字符数组中是否所有的数字都只出现过一次
给定一个个数字arr,判断数组arr中是否所有的数字都只出现过一次。
1. 时间复杂度为
遍历arr,用map记录数字的出现情况
2. 保证额外空间复杂度为的前提下,时间复杂度尽量低的方法。
将arr排序,再判断有没有重复数字。空间复杂度为,只能使用非递归的堆排序算法。
面试题51.数组中的逆序对
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
输入: [7,5,6,4]
输出: 5
使用归并排序解决。在merge时,若右数组的当前值比左数组的当前值大,则右数组和左数组剩下的数字全都为逆序对。本题即是在归并排序的基础上,计算出逆序对的个数。
class Solution {
public int reversePairs(int[] nums) {
if(nums.length==0) {
return 0;
}
return mergeSort(nums,0,nums.length-1);
}
public int mergeSort(int[] nums,int left,int right) {
if (left == right) {
return 0;
}
int mid = left + (right-left) /2;
int ans = mergeSort(nums,left,mid);
ans += mergeSort(nums,mid+1,right);
// merge
int[] tmp = new int[right - left + 1];
int pleft = left;
int pright = mid+1;
for(int i=0;i<tmp.length;i++) {
if(pleft > mid) {
tmp[i] = nums[pright++];
} else if (pright > right) {
tmp[i] = nums[pleft++];
} else {
if(nums[pleft] <= nums[pright]) {
tmp[i] = nums[pleft++];
} else { //右边的小
ans += (mid-pleft+1); //逆序
tmp[i] = nums[pright++];
}
}
}
// 赋值给原数组
for(int i=0;i<tmp.length;i++) {
nums[left+i] = tmp[i];
}
return ans;
}
}