算法与数据结构 排序

目录

 

● 请问Java中collection的sort方法,默认的排序方法是什么

参考回答:

● 手写代码:合并两个排序数组

参考回答:

● 请问有哪些排序算法

参考回答:

● 手写代码:冒泡排序

参考回答:

● 手写代码:统计排序数组中出现次数最多的元素出现的次数?

参考回答:

● 请你说一下堆排序的思想?以及怎么初始建堆?是否稳定?

参考回答:

● 手写代码:数组的2-sum,3-sum,问题(leetcode原题)

参考回答:

● 手写代码:5个扑克牌是否是顺子,大小王当成任意的

参考回答:

● 请你说一说快速排序,并手写代码

参考回答:

● 你最熟悉什么算法?给我说一下原理或者排序过程?它的优缺点是什么?你知道什么排序算法,介绍他们的实现方法,时间复杂度和空间复杂度,是否稳定,快排基准点怎么选择,

参考回答:

● 请你说一下快排如何实现?

参考回答:


● 请问Java中collection的sort方法,默认的排序方法是什么

参考回答:

排序方法是归并排序

● 手写代码:合并两个排序数组

参考回答:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

public static int[] MergeList(int a[],int b[])

    {

    int result[];

    //                定义一个新数组,长度为两个数组长度之和

    result = new int[a.length+b.length];

    //i:a数组下标    j:b数组下标  k:新数组下标

    int i=0,j=0,k=0;

    //                按位循环比较两个数组,较小元素的放入新数组,下标加一(注意,较大元素对应的下标不加一),直到某一个下标等于数组长度时退出循环

    while(i<a.length && j<b.length)

    if(a[i] <= b[j]) {

    result[k++] = a[i++];

    print(result);

    System.out.println();

    }else{

    result[k++] = b[j++];

    }

    /* 后面连个while循环是用来保证两个数组比较完之后剩下的一个数组里的元素能顺利传入 *

    * 此时较短数组已经全部放入新数组,较长数组还有部分剩余,最后将剩下的部分元素放入新数组,大功告成*/

    while(i < a.length)

    result[k++] = a[i++];

    while(j < b.length)

    result[k++] = b[j++];

    return result;

● 请问有哪些排序算法

参考回答:

冒泡排序

是最简单的排序之一了,其大体思想就是通过与相邻元素的比较和交换来把小的数交换到最前面。这个过程类似于水泡向上升一样,因此而得名。举个栗子,对5,3,8,6,4这个无序序列进行冒泡排序。首先从后向前冒泡,4和6比较,把4交换到前面,序列变成5,3,8,4,6。同理4和8交换,变成5,3,4,8,6,3和4无需交换。5和3交换,变成3,5,4,8,6.这样一次冒泡就完了,把最小的数3排到最前面了。对剩下的序列依次冒泡就会得到一个有序序列。冒泡排序的时间复杂度为O(n^2)。

选择排序

思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择。举个栗子,对5,3,8,6,4这个无序序列进行简单选择排序,首先要选择5以外的最小数来和5交换,也就是选择3和5交换,一次排序后就变成了3,5,8,6,4.对剩下的序列一次进行选择和交换,最终就会得到一个有序序列。其实选择排序可以看成冒泡排序的优化,因为其目的相同,只是选择排序只有在确定了最小数的前提下才进行交换,大大减少了交换的次数。选择排序的时间复杂度为O(n^2)

插入排序

不是通过交换位置而是通过比较找到合适的位置插入元素来达到排序的目的的。相信大家都有过打扑克牌的经历,特别是牌数较大的。在分牌时可能要整理自己的牌,牌多的时候怎么整理呢?就是拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是一样的。举个栗子,对5,3,8,6,4这个无序序列进行简单插入排序,首先假设第一个数的位置时正确的,想一下在拿到第一张牌的时候,没必要整理。然后3要插到5前面,把5后移一位,变成3,5,8,6,4.想一下整理牌的时候应该也是这样吧。然后8不用动,6插在8前面,8后移一位,4插在5前面,从5开始都向后移一位。注意在插入一个数的时候要保证这个数前面的数已经有序。简单插入排序的时间复杂度也是O(n^2)。

(我用了链表,别人用数组后移更好)

快速排序

在实际应用当中快速排序确实也是表现最好的排序算法。快速排序虽然高端,但其实其思想是来自冒泡排序,冒泡排序是通过相邻元素的比较和交换把最小的冒泡到最顶端,而快速排序是比较和交换小数和大数,这样一来不仅把小数冒泡到上面同时也把大数沉到下面。

对5,3,8,6,4这个无序序列进行快速排序,思路是右指针找比基准数小的,左指针找比基准数大的,交换之。

5,3,8,6,4 用5作为比较的基准,最终会把5小的移动到5的左边,比5大的移动到5的右边。

5,3,8,6,4 首先设置i,j两个指针分别指向两端,j指针先扫描(思考一下为什么?)4比5小停止。然后i扫描,8比5大停止。交换i,j位置。

5,3,4,6,8 然后j指针再扫描,这时j扫描4时两指针相遇。停止。然后交换4和基准数。

4,3,5,6,8 一次划分后达到了左边比5小,右边比5大的目的。之后对左右子序列递归排序,最终得到有序序列。

上面留下来了一个问题为什么一定要j指针先动呢?首先这也不是绝对的,这取决于基准数的位置,因为在最后两个指针相遇的时候,要交换基准数到相遇的位置。一般选取第一个数作为基准数,那么就是在左边,所以最后相遇的数要和基准数交换,那么相遇的数一定要比基准数小。所以j指针先移动才能先找到比基准数小的数。快速排序是不稳定的,其时间平均时间复杂度是O(nlgn)。

堆排序

借助堆来实现的选择排序,思想同简单的选择排序,以下以大顶堆为例。注意:如果想升序排序就使用大顶堆,反之使用小顶堆。原因是堆顶元素需要交换到序列尾部。实现堆排序需要解决两个问题:

1. 如何由一个无序序列建成一个堆?

2. 如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?

堆(二叉堆)可以视为一棵完全的二叉树,完全二叉树的一个“优秀”的性质是,除了最底层之外,每一层都是满的,这使得堆可以利用数组来表示(普通的一般的二叉树通常用链表作为基本容器表示),每一个结点对应数组中的一个元素。

对于给定的某个结点的下标i,可以很容易的计算出这个结点的父结点、孩子结点的下标:

Parent(i) = floor(i/2),i 的父节点下标

Left(i) = 2i,i 的左子节点下标

Right(i) = 2i + 1,i 的右子节点下标

堆排序(Heap-Sort)是堆排序的接口算法,Heap-Sort先调用Build-Max-Heap将数组改造为最大堆,然后将堆顶和堆底元素交换,之后将底部上升,最后重新调用Max-Heapify保持最大堆性质。

希尔排序

插入排序的一种高效率的实现,也叫缩小增量排序。简单的插入排序中,如果待排序列是正序时,时间复杂度是O(n),如果序列是基本有序的,使用直接插入排序效率就非常高。基本思想是:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。

希尔排序的特点是,子序列的构成不是简单的逐段分割,而是将某个相隔某个增量的记录组成一个子序列。如上面的例子,第一堂排序时的增量为5,第二趟排序的增量为3。

归并排序

使用了递归分治的思想,先递归划分子问题,然后合并结果。把待排序列看成由两个有序的子序列,然后合并两个子序列,然后把子序列看成由两个有序序列。。。。。倒着来看,其实就是先两两合并,然后四四合并。。。最终形成有序序列。空间复杂度为O(n),时间复杂度为O(nlogn)。

计数排序

如果在面试中有面试官要求你写一个O(n)时间复杂度的排序算法,你千万不要立刻说:这不可能!虽然前面基于比较的排序的下限是O(nlogn)。但是确实也有线性时间复杂度的排序,只不过有前提条件,就是待排序的数要满足一定的范围的整数,而且计数排序需要比较多的辅助空间。其基本思想是,用待排序的数作为计数数组的下标,统计每个数字的个数。然后依次输出即可得到有序序列。

(java中有整型的最大最小值可以直接用)

int MAX = Integer.MAX_VALUE;

int MIN = Integer.MIN_VALUE;

桶排序

假设有一组长度为N的待排关键字序列K[1....n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]….B[M]中的全部内容即是一个有序序列。

基数排序

一种借助多关键字排序思想对单逻辑关键字进行排序的方法。所谓的多关键字排序就是有多个优先级不同的关键字。比如说成绩的排序,如果两个人总分相同,则语文高的排在前面,语文成绩也相同则数学高的排在前面。。。如果对数字进行排序,那么个位、十位、百位就是不同优先级的关键字,如果要进行升序排序,那么个位、十位、百位优先级一次增加。基数排序是通过多次的收分配和收集来实现的,关键字优先级低的先进行分配和收集。

二分法插入排序

二分法插入排序是在插入第i个元素时,对前面的0~i-1元素进行折半,先跟他们中间的那个元素比,如果小,则对前半再进行折半,否则对后半进行折半,直到left<right,然后再把第i个元素前1位与目标位置之间的所有元素后移,再把第i个元素放在目标位置上。

二分插入排序是稳定的与二分查找的复杂度相同;

最好的情况是当插入的位置刚好是二分位置所用时间为O(log₂n);

最坏的情况是当插入的位置不在二分位置所需比较次数为O(n),无限逼近线性查找的复杂度。

● 手写代码:冒泡排序

参考回答:

void bubblesort(int[] num){

for(int i=0;i<num.length-1;i++) {

for(int j=num.length-1;j>i;j--) {

if(num[j]<num[j-1])swap(num,j,j-1);

}

}

for(int i=0;i<num.length;i++) {

System.out.print(num[i]);

}

}

● 手写代码:统计排序数组中出现次数最多的元素出现的次数?

参考回答:

public class Main {

static void findmost(int[] array){

int lastEle=array[0];

int maxTime=0;

int presentTime=1;

int maxEle=array[0];

for(int i=1;i<array.length;i++)

{

if(array[i]==lastEle)

presentTime++;

else

{

if(presentTime>maxTime)

{

maxTime=presentTime;

maxEle=lastEle;

}

lastEle=array[i];

presentTime=1;

}

if(i==array.length-1 && presentTime>maxTime)// 考虑到比较到最大的元素(排在最后的元素),需要在循环推出前比较一次

{

maxTime=presentTime;

maxEle=lastEle;

}

}

System.out.println("出现次数最多的元素"+maxEle+" "+"出现的次数"+maxTime);

}

public static void main(String args[]) {

int[] array= {1,1,2,2,2,3,4,5,6,6,6,6,6,7,8,8,9};

Main.findmost(array);

}

}

● 请你说一下堆排序的思想?以及怎么初始建堆?是否稳定?

参考回答:

堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它是不稳定的排序。

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:

对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子

该数组从逻辑上讲就是一个堆结构,用简单的公式来描述一下堆的定义就是:

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了

步骤一构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。

假设给定无序序列结构如下

从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。

找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。

这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。

此时,我们就将一个无需序列构造成了一个大顶堆。

步骤二将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

a.将堆顶元素9和末尾元素4进行交换

b.重新调整结构,使其继续满足堆定义

c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序

● 手写代码:数组的2-sum,3-sum,问题(leetcode原题)

参考回答:

2SUM问题

最常见的是2SUM问题(1. 2SUM),就是数组中找出两个数相加和为给定的数。 
这道题有两种思路: 
1. 一种思路从首尾搜索两个数的和,并逐步逼近。 
2. 另外一种思路是固定一个数A,看SUM-A是否在这个数组之中。

对于第一种思路如下:

此方法是先将数组从小到大排序 
设置两个指针,一个指向数组开头,一个指向数组结尾,从两边往中间走。直到扫到满足题意的为止或者两个指针相遇为止。 
此时这种搜索方法就是类似于杨氏矩阵的搜索方法,就是从杨氏矩阵的左下角开始搜索,直到找到为止。 
如果此时题目条件变为如果没有找出最接近的2SUM和问题,解决方法跟上面是一样的 
用这种方法2SUM问题的时间复杂度是O(nlogn)O(nlog⁡n)的,主要在于排序的时间。

第二种思路方法如下:

对数组中的每个数建立一个map/hash_map 然后再扫一遍这个数组,判断target-nums[i]是否存在,如果存在则说明有,不存在继续找。当然这样做的话,需要处理一个细节:判重的问题。 
代码如下【注意因为题目中说一定有解所以才下面这样写,如果不一定有解,则还要再加一些判断】

vector<int> twoSum(vector<int>& nums, int target) {

unordered_map<int,vector<int>> mark;

for(int i=0;i<nums.size();i++)

mark[nums[i]].push_back(i);

for(int i = 0;i<nums.size();i++){

if(target-nums[i] == nums[i]){

if(mark[nums[i]].size() > 1){

vector<int> tmp{i,mark[nums[i]][1]};

return tmp;

}

}else{

if(mark.find(target-nums[i]) != mark.end()){

vector<int> tmp{i,mark[target-nums[i]][0]};

return tmp;

}

}

}

}

3-SUM 问题

Question

Given an array nums of n integers, are there elements a, b, c in nums such that a + b + c = 0? Find all unique triplets in the array which gives the sum of zero.The solution set must not contain duplicate triplets.

根据给定的容量为 n 的整数数组,找到所有满足 a + b + c = 0 的三个元素a、b 、c组合,需去重;

解法

修改问题为:满足 a + b + c = target,target是给定数,原题即 target = 0;

根据题目可知,与 2-SUM 问题类似,在整数数组中不放回的取出三个元素,其和等于给定数(0),不同的是,满足条件的解有多个而且需要去重;

首先想到的解法是,遍历数组,然后调用 2-SUM 问题中的方法寻找两个元素相加等于当前元素的补足,最后执行去重操作;这样的话,查询的时间复杂度是$O(n^2)$,空间复杂度是$O(n^2)$,去重的时间复杂度是$O(n^2)$,空间复杂度是$O(1)$,这绝对不能算一个好方案;

思考其他思路,既然要去重,可以先对数组执行一次排序,这样的话在遍历的时候可以跳过相同元素;在确定一个当前元素后,找另外两个元素相加作为当前元素的补足,此时的解可能是多个的,采用首尾标记的方式可以一次遍历完成查找;

public List<List<Integer>> threeSum(int[] nums, int target){

int length = nums.length;

List<List<Integer>> result = new ArrayList<>();

Arrays.sort(nums);

for(int i = 0; i < length - 2; i++) {

if(nums[i] + nums[i+1] + nums[i+2] > target)break; // too large

if(nums[i] + nums[length-1] + nums[length-2] < target)continue; // too small

if(i > 0 && nums[i] == nums[i - 1]) continue;

int left = i + 1;

int right = length - 1;

while(left < right){

int diff = target - nums[i] - nums[left] - nums[right];

if (diff == 0){

result.add(new ArrayList<Integer>(Arrays.asList(nums[i], nums[left], nums[right])));

while(left < right && nums[left] == nums[left + 1]) left++;

while(left < right && nums[right] == nums[right - 1]) right--;

left++;

right--;

}else if (diff > 0){

left++;

}else {

right--;

}

}

}

return result;

}

● 手写代码:5个扑克牌是否是顺子,大小王当成任意的

参考回答:

这是剑指offer原题

从扑克牌中随机抽出5张牌,判断是不是一个顺子,即这五张牌是不是连续的。2——10为数字本身,A为1,J为11,Q为12,K为13,而大小王为任意数字。

package org.marsguo.offerproject44;

import java.util.Scanner;

class SolutionMethod1{

public void sortfun(int[] arrays,int start, int end){

int numOfZero=0;

if(start>=end){                                //判断数组的起始和终止是否相同,相同表示已经都全部排完,返回

return;

}

int i = start;                                //i指向数组的起始位

int j = end;                                //j指向数组的末位

int key = arrays[i];                        //选取数组的第一位为关键字key,基准元素

boolean flag = true;                        //设置标志位,用于判断是i++还是j--;这个很重要

while(i != j){                                //如果i≠j,表示还没有比较完,即即关键字左右两侧还不是最小与最大

if(flag){

if(key>arrays[j]){                    //从后向前遍历,找到小于key的值,

swap(arrays,i,j);                //找到小于key的值后将arrays[i]与此值交换

flag = false;

}else{                                //如果没有找到的话j--,向前遍历

j--;

}

}else{

if(key<arrays[i]){                    //从前向后遍历,找到大于key的值

swap(arrays,i,j);                //将此值与arrays[j]进行交换

flag = true;

}else{                                //如果没有找到话就将i++,向后遍历

i++;

}

}

}

//sprint(arrays);                                //打印每次排序后的数组

sortfun(arrays,start,j-1);                    //递归调用,将基准元素的前半段数组再用此方法进行排序,直到所有都排完为止。

sortfun(arrays,i+1,end);                    //递归调用,将基准元素的后半段数组再用此方法进行排序,直到所有都排完为止。

//System.out.println("排序后的数组是:");

for(int k = 0; k < arrays.length; k++){

if(arrays[k] == 0){

numOfZero++;

}

//System.out.print("numOfZero= " + numOfZero + ";"+arrays[k] + ", ");

//System.out.print(arrays[k] + ", ");

}

IsContinuousFun(arrays, numOfZero);

}

public void swap(int[] array,int i,int j){

int temp;

temp = array[i];

array[i] = array[j];

array[j] = temp;

}

public void IsContinuousFun(int[] array,int numOfZero){

int numOfGap = 0;                //判断数字中间空了多少个0

//System.out.println("numberOfZero = " + numOfZero);

for(int j = 1; j < array.length; j++){

int flag = array[j] - array[j-1];        //用排序数组中后一个数字-前一个数字

if(array[j] == array[j-1] && array[j] != 0){        //如果数组中有重复的非0数字,则不是顺子,退出

System.out.println("有重复数字,不是顺子牌");

return;

}

else if(flag != 1 && flag != 0 && array[j-1] != 0){            //判断不是连续的数字,也不是0,

numOfGap += flag-1;                                 //非0数字间缺少的数字的个数

}

}

if(numOfZero != numOfGap){

System.out.println("这不是一个顺子扑克牌");

}

else{

System.out.println("这是一张顺子扑克牌");

}

}

}

public class IsContinuous {

public static void main(String[] args){

Scanner scanner = new Scanner(System.in);                        //扫描键盘输入

System.out.println("请输入五张牌:");

String str = scanner.nextLine();                                //将键盘输入转化为字符串

String[] temp = str.split(" ");                                    //将字符串用“ ”分开转化为字符串数组

scanner.close();

int[] array = new int[temp.length];                                //定义一个整型数组array

for(int i = 0; i< temp.length; i++){                            //将字符串数组强制转化为整型数组

array[i] = Integer.parseInt(temp[i]);                        //这种方法非常巧妙

}

SolutionMethod1 solution1 = new SolutionMethod1();

//solution1.quicksort(array, 0, array.length-1);

solution1.sortfun(array, 0, array.length-1);

}

}

● 请你说一说快速排序,并手写代码

参考回答:

1、快速排序的基本思想:

快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

2、快速排序的三个步骤:

(1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”(pivot)

(2)分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大

(3)递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。

3、选择基准的方式:

对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列三种选择基准的方法:

方法(1):固定位置

思想:取序列的第一个或最后一个元素作为基准

方法(2):随机选取基准

引入的原因:在待排序列是部分有序时,固定选取枢轴使快排效率底下,要缓解这种情况,就引入了随机选取枢轴

思想:取待排序列中任意一个元素作为基准

方法(3):三数取中(median-of-three)

引入的原因:虽然随机选取枢轴时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取枢轴

分析:最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为枢纽元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。显然使用三数中值分割法消除了预排序输入的不好情形,并且减少快排大约14%的比较次数。

固定位置示例程序:

public class QuickSort {

public static void main(String[] args) {

int[] a = {1, 2, 4, 5, 7, 4, 5 ,3 ,9 ,0};

System.out.println(Arrays.toString(a));

quickSort(a);

System.out.println(Arrays.toString(a));

}

public static void quickSort(int[] a) {

if(a.length>0) {

quickSort(a, 0 , a.length-1);

}

}

private static void quickSort(int[] a, int low, int high) {

//1,找到递归算法的出口

if( low > high) {

return;

}

//2, 存

int i = low;

int j = high;

//3,key

int key = a[ low ];

//4,完成一趟排序

while( i< j) {

//4.1 ,从右往左找到第一个小于key的数

while(i<j && a[j] > key){

j--;

}

// 4.2 从左往右找到第一个大于key的数

while( i<j && a[i] <= key) {

i++;

}

//4.3 交换

if(i<j) {

int p = a[i];

a[i] = a[j];

a[j] = p;

}

}

// 4.4,调整key的位置

int p = a[i];

a[i] = a[low];

a[low] = p;

//5, 对key左边的数快排

quickSort(a, low, i-1 );

//6, 对key右边的数快排

quickSort(a, i+1, high);

}

}

四种优化方式:

优化1:当待排序序列的长度分割到一定大小后,使用插入排序

原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排

优化2:在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割

优化3:优化递归操作

快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化

优点:如果待排序的序列划分极端不平衡,递归的深度将趋近于n,而栈的大小是很有限的,每次递归调用都会耗费一定的栈空间,函数的参数越多,每次递归耗费的空间也越多。优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(logn),将会提高性能。

优化4:使用并行或多线程处理子序列

● 你最熟悉什么算法?给我说一下原理或者排序过程?它的优缺点是什么?你知道什么排序算法,介绍他们的实现方法,时间复杂度和空间复杂度,是否稳定,快排基准点怎么选择,

参考回答:

最熟悉排序算法,常见的排序算法有以下几种

1、插入排序,即逐个处理待排序的记录,每个记录与前面已排序的子序列进行比较,将它插入子序列中正确位置,时间复杂度O(n^2),空间复杂度为O(1),是稳定排序,插入排序不适合对于数据量比较大的排序应用,但是如果量级小余千,那么插入排序还是一个不错的选择,

2、冒泡排序,它重复地走访过要排序的元素,依次比较相邻两个元素,如果他们的顺序错误就把他们调换过来,直到没有元素再需要交换,排序完成。这个算法的名字由来是因为越小(或越大)的元素会经由交换慢慢“浮”到数列的顶端。他的时间复杂度和空间复杂度分别是O(n^2),空间复杂度是O(1)属于稳定排序,冒泡排序对于少数元素之外的数列排序是很没效率的。

3、选择排序,初始时在序列中找到最值元素,放到序列的其实位置作为已排序序列,再在剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。他的时间复杂度是O(n^2),空间复杂度是O(1)属于不稳定排序

4、shell排序,是插入排序的升级版,属于不稳定排序,希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O(n^2)的排序(冒泡排序或直接插入排序),可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。希尔排序的时间复杂度根据步长序列的不同而不同,空间复杂度O(1)

5、归并排序,归并排序的实现分为递归实现与非递归(迭代)实现。属于稳定排序,递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。他的时间复杂度是O(nlogn)空间复杂度是O(n)

6、堆排序,其实现原理为首先将数组构造成一个最大/最小堆,将堆顶元素和堆尾元素互换,调出堆顶元素,重新构造堆,重复步骤直至堆中元素都被调出。堆排序的时间复杂度为O(nlogn)空间复杂度为O(1),属于不稳定排序。

7、快速排序,快排使用分治策略,首先从序列中挑出一个元素作为基准,然后把比基准小的元素放在一边,把比基准大的元素放在另一边,重复这个步骤,直至子序列的大小是0/1.快排的时间复杂度是O(nlogn)空间复杂度是O(logn)属于不稳定算法,对于快排的基准元素选取,可以采用三者取中法,三个随机值的中间一个。为了减少随机数生成器产生的延迟,可以选取首中尾三个元素作为随机值

排序算法的适用情况:

当n比较小且基本有序,可采用直接插入或冒泡,否则采用直接插入

当n较大,可以采用快排,归并,堆排序,快排是目前基于比较的内部排序中最好的方法,堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。 
若要求排序稳定,则可选用归并排序。但前面介绍的从单个记录起进行两两归并的排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子序列,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。

● 请你说一下快排如何实现?

参考回答:

首先选择一个轴值,小于轴值的元素被放在数组中轴值左侧,大于轴值的元素被放在数组中轴值右侧,这称为数组的一个分割(partition)。快速排序再对轴值左右子数组分别进行类似的操作

选择轴值有多种方法。最简单的方法是使用首或尾元素。但是,如果输入的数组是正序或者逆序时,会将所有元素分到轴值的一边。较好的方法是随机选取轴值

代码:

template <class Elem>

int partition(Elem A[],int i,int j)

{

//这里选择尾元素作为轴值,轴值的选择可以设计为一个函数

//如果选择的轴值不是尾元素,还需将轴值与尾元素交换

int pivot = A[j];

int l = i - 1;

for(int r = i;r < j;r++)

if(A[r] <= pivot)

swap(A,++l,r);

swap(A,++l,j);//将轴值从末尾与++l位置的元素交换

return l;

}

template <class Elem>

void qsort(Elem A[],int i,int j)

{

if(j <= i)  return;

int p = partition<Elem>(A,i,j);

qsort<Elem>(A,i,p - 1);

qsort<Elem>(A,p + 1,j);

}

猜你喜欢

转载自blog.csdn.net/u012369559/article/details/89511288