面试常用排序算法+LeetCode排序算法应用

排序算法也是面试中常常提及的内容,问的最多的应该是快速排序、堆排序、归并排序。这些排序算法很基础,很可能知道其思想,但在写代码时细节上出错。因此本文的书写主要参考了十大经典排序算法动画与解析,看我就够了!(配代码完全版),对基于比较的排序算法:冒泡排序、插入排序、选择排序以及归并排序、快速排序和堆排序重新自己实现了一遍。上文中给出的算法动画有助于排序算法的理解。以下算法全部基于数组原地排序。

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);
        }
    }

}

给出一个表格总结排序算法的时空复杂度和稳定情况

排序算法平均时间复杂度最坏时间复杂度空间复杂度是否稳定
冒泡排序O(n^{2})O(n^{2})O(1)

插入排序

O(n^{2})O(n^{2})O(1)
选择排序O(n^{2})O(n^{2})O(1)
归并排序O(nlogn)O(nlogn)O(n)
快速排序O(nlogn)O(n^{2})O(logn)
堆排序O(nlogn)O(nlogn)递归 O(logn)
非递归 O(1)
  • 排序算法的稳定性:排序前后相同元素的相对位置不变,则称排序算法是稳定的;否则排序算法是不稳定的。
  • 归并排序需要使用辅助数组来合并两个数组,空间复杂度为O(n)
  • 快速排序递归方法使用函数栈空间,空间复杂度平均为O(logn),最差为O(n);非递归方法仍需使用自定义栈,空间复杂度为O(logn)
  • 堆排序递归方法使用函数栈空间,空间复杂度为O(logn);非递归方法空间复杂度为O(1)

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. 时间复杂度为O(N)

遍历arr,用map记录数字的出现情况

2. 保证额外空间复杂度为O(1)的前提下,时间复杂度尽量低的方法。

将arr排序,再判断有没有重复数字。空间复杂度为O(1),只能使用非递归的堆排序算法。

面试题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;
    }
}

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值