在线编程——排序算法总结

                                     在线编程——排序算法总结

        找实习,阿里一面遇到手写快排,写出来感觉没错(VS2013能通过),但在阿里的测试平台上运行未通过。细思极恐,赶紧总结一波。有幸看到SteveWang的两篇博客:排序算法总结(1)与排序算法总结(2)以及基数排序、计数排序与桶排序,总结的相当详细,我这里算是重新拜读一遍,结合自己的理解写下来。

一、排序算法分类,稳定性分析,时间复杂度与空间复杂度总结表

       我们通常所说的排序算法往往指的是内部排序算法,即数据记录在内存中进行排序。而外部排序是指大文件的排序,即待排序的记录存储在外存储器上,在排序过程中需要进行多次的内、外存之间的交换。

      (一)内部排序算法大体可分为两大类:

        1、比较排序,平均时间复杂度范围:O(nlogn) ~ O(n^2),主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。

然而,参考博客:https://blog.csdn.net/zlele0326/article/details/51281578,比较排序算法可以继续分为下面几类:

(1)插入排序:直接插入排序,二分法插入排序,希尔排序;

(2)选择排序:简单选择排序,堆排序;

(3)交换排序:冒泡排序,快速排序;

(4)归并排序;

  2、非比较排序,时间复杂度可以达到O(n),主要有:计数排序基数排序桶排序等。详见地址:戳我。

(二)稳定性分析

        排序算法稳定性的简单形式化定义为:如果Ai = Aj,排序前Ai在Aj之前,排序后Ai还在Aj之前,则称这种排序算法是稳定的。通俗地讲就是保证排序前后两个相等的数的相对顺序不变。

  对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性;而对于稳定的排序算法,必须对算法进行分析从而得到稳定的特性。需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。

  例如,对于冒泡排序,原本是稳定的排序算法,如果将记录交换的条件改成A[i] >= A[i + 1],则两个相等的记录就会交换位置,从而变成不稳定的排序算法。

  其次,说一下排序算法稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位排序后元素的顺序在高位也相同时是不会改变的。

(三)排序算法总结表

      尽管记了很多次几个排序算法的时间复杂度与空间复杂度,日子一长又忘了,心累,各种排序的稳定性,时间复杂度、空间复杂度、稳定性总结如下图(参考:https://blog.csdn.net/foreverling/article/details/43798223):

在线编程——排序算法总结_第1张图片

                                                                                                              图1

在线编程——排序算法总结_第2张图片

                                                                                                               图2

        网上现主要有两个版本的排序表格,如上图1,图2(来自《大话数据结构》)所示。可以看到两张图中希尔排序的时间复杂度不同,以及快速排序的空间复杂度不同,那究竟是什么?

(1)希尔排序时间复杂度分析:(摘自百度百科),希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n^2),而Hibbard增量的希尔排序时间复杂度为O(n^3/2),希尔排序时间复杂度的下界是n*log2n。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择,但是比O( n^2 )复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法. 希尔排序算法的性能与所选取的分组长度序列有很大关系。只对特定的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数。想要弄清关键词比较次数和记录移动次数与增量选择之间的关系,并给出完整的数学分析,至今仍然是数学难题。

(2)快速排序空间复杂度分析:详见博客(https://blog.csdn.net/yuzhihui_no1/article/details/44198701),首先就地快速排序使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据,即快速排序的空间复杂度为:O(logn) ~O( n ) 。

最优的情况下空间复杂度为:O(logn)  ;每一次都平分数组的情况

最差的情况下空间复杂度为:O( n )      ;退化为冒泡排序的情况

二、比较排序

1、直接插入排序

插入排序是一种简单直观的排序算法。它的工作原理非常类似于我们抓扑克牌

在线编程——排序算法总结_第3张图片

        对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

  具体算法描述如下:

       (1) 从第一个元素开始,该元素可以认为已经被排序;

       (2) 取出下一个元素,在已经排序的元素序列中从后向前扫描;

       (3) 如果该元素(已排序)大于新元素,将该元素移到下一位置;

       (4) 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;

       (5) 将新元素插入到该位置后;

       (6) 重复步骤2~5;

C++代码为:

#include 
#include 
using namespace std;

// 分类 ------------- 内部比较排序
// 数据结构 ---------- 向量
// 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
// 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定

vector InsertionSort(vector A)
{
	for (int i = 1; i < A.size(); i++)         // 类似抓扑克牌排序
	{
		int get = A[i];                 // 右手抓到一张扑克牌
		int j = i - 1;                  // 拿在左手上的牌总是排序好的
		while (j >= 0 && A[j] > get)    // 将抓到的牌与手牌从右向左进行比较
		{
			A[j + 1] = A[j];            // 如果该手牌比抓到的牌大,就将其右移
			j--;
		}
		A[j + 1] = get; // 直到该手牌比抓到的牌小(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
	}
	return A;
}

int main()
{
	//输入不定长的待排序列
	vector vec;
	int temp;
	char t;
	cout << "输入待排序列为:";
	do{
		cin >> temp;
		vec.push_back(temp);
	} while ((t=cin.get())!='\n');
	/////////////////////////////////////////////////////////////////////////////////////
	int n = vec.size();//待排序列的长度 ,若输入为数组A,则int n = sizeof(A) / sizeof(int);
	cout << "排序前的结果为:";
	for (int i = 0; i < n; i++)
	{
		cout << vec[i] << " ";
	}
	cout << endl;
	/////////////////////////////////////////////////////////////////////////////////////
	vector res = InsertionSort(vec); //调用直接插入排序算法
	cout << "排序后的结果为:";
	for (int i = 0; i < n; i++)
	{
		cout << res[i] << " ";
	}
	cout << endl;
	system("pause");
	return 0;
}

运行结果:

在线编程——排序算法总结_第4张图片

插入排序过程:动态图来自http://www.cnblogs.com/eniac12/p/5329396.html#3972917

                                 在线编程——排序算法总结_第5张图片在线编程——排序算法总结_第6张图片

        插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。

python代码如下:

def InsertSort(myList):
    length = len(myList)
    for i in range(1,length):
        # 设置当前值前一个元素的标识
        j = i - 1
        
        #如果当前值小于前一个元素,则将当前值作为一个临时变量存储,将前一个元素后移一位
        if(myList[i] < myList[j]):
             temp = myList[i]
             myList[i] = myList[j]
             
             # 继续往前寻找,如果有比临时变量大的数字,则后移一位,直到找到比临时变量小的元素或者达到列表第一个元素
             j = j-1
             while j>=0 and myList[j] > temp:
                 myList[j+1] = myList[j]
                 j = j-1
                 
             # 将临时变量赋值给合适位置
             myList[j+1] = temp

if __name__=="__main__":
    arr = [int(i) for i in input().split()] # 输入待排序列
    InsertSort(arr)                         # 插入排序
    print(" ".join(str(i) for i in arr))    # 输出排序结果

运行结果:

在线编程——排序算法总结_第7张图片

 

2、快速排序算法

    快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。

  快速排序使用分治策略把一个序列分为两个子序列。步骤为:

(1)从序列中挑出一个元素,作为"基准"(pivot).

(2)把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。

(3)对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。

C++代码:

#include 
#include 
#include  
using namespace std;
// 分类 ------------ 内部比较排序
// 数据结构 --------- 向量
// 最差时间复杂度 ---- 每次选取的基准都是最大(或最小)的元素,导致每次只划分出了一个分区,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
// 最优时间复杂度 ---- 每次选取的基准都是中位数,这样每次都均匀的划分出两个分区,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ 主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)       
// 稳定性 ---------- 不稳定

void Swap(vector& data, int i, int j)
{
	int temp = data[i];
	data[i] = data[j];
	data[j] = temp;
}

int RandomInRange(int min, int max)   //在min~max中随机生成一个值  
{
	int random = rand() % (max - min + 1) + min;
	return random;
}

int Partition(vector& data, int start, int end) //Partition函数
{
	if (data.size() <= 0 || start < 0 || end >= data.size()) 
		throw new std::exception("Invalid Parameters");

	//int index = RandomInRange(start, end); //可以不调用RandomInRange函数
	//Swap(data, index, end);

	int small = start - 1;
	for (int index = start; index < end; ++index)
	{
		if (data[index] < data[end])
		{
			++small;
			if (small != index)
				Swap(data, index, small);
		}
	}
	++small;
	Swap(data, small, end);
	return small;
}

//递归快速排序  
void QuickSort(vector& data, int start, int end)
{
	if (start >= end)
		return;
	int index = Partition(data, start, end); //基准的索引
	QuickSort(data, start, index - 1);
	QuickSort(data, index + 1, end);
}

int main()
{
	//输入不定长的待排序列
	vector vec;
	int temp;
	char t;
	cout << "输入待排序列为:";
	do{
		cin >> temp;
		vec.push_back(temp);
	} while ((t = cin.get()) != '\n');
	/////////////////////////////////////////////////////////////////////////////////////
	int n = vec.size();//待排序列的长度 ,若输入为数组A,则int n = sizeof(A) / sizeof(int);

	cout << "排序前的结果为:";
	for (int i = 0; i < n; i++)
	{
		cout << vec[i] << " ";
	}
	cout << endl;
	/////////////////////////////////////////////////////////////////////////////////////
	QuickSort(vec, 0, n - 1); //调用快速排序算法
    /////////////////////////////////////////////////////////////////////////////////////
	cout << "排序后的结果为:";
	for (int i = 0; i < n; i++)
	{
		cout << vec[i] << " ";
	}
	cout << endl;
	system("pause");
	return 0;
}

运行结果:

快速排序过程展示:

在线编程——排序算法总结_第8张图片

    快速排序是不稳定的排序算法,不稳定发生在基准元素与A[small+1]交换的时刻。

  比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基准元素是5,一次划分操作后5要和第一个8进行交换,从而改变了两个元素8的相对次序。

  Java系统提供的Arrays.sort函数。对于基础类型,底层使用快速排序。对于非基础类型,底层使用归并排序。请问是为什么?

      答:这是考虑到排序算法的稳定性。对于基础类型,相同值是无差别的,排序前后相同值的相对位置并不重要,所以选择更为高效的快速排序,尽管它是不稳定的排序算法;而对于非基础类型,排序前后相等实例的相对位置不宜改变,所以选择稳定的归并排序。 

       需要强调的是,很多公司的面试官喜欢在面试环节要求应聘者写出快速排序的代码,剑指offer上一个比较好的方法,保存了数组的两个位置index向前遍历数组,small用于保存交换的小于轴值的数,找到一个前移一步,即上面的函数:

void Swap(vector &data, int i, int j)
{
	int temp = data[i];
	data[i] = data[j];
	data[j] = temp;
}

int RandomInRange(int min, int max)   //在min~max中随机生成一个值  
{
	int random = rand() % (max - min + 1) + min;
	return random;
}

int Partition(vector &data, int start, int end) //Partition函数
{
	if (data.size() <= 0 || start < 0 || end >= data.size()) 
		throw new std::exception("Invalid Parameters");

	//int index = RandomInRange(start, end);
	//Swap(data, index, end);

	int small = start - 1;
	for (int index = start; index < end; ++index)
	{
		if (data[index] < data[end])
		{
			++small;
			if (small != index)
				Swap(data, index, small);
		}
	}
	++small;
	Swap(data, small, end);
	return small;
}

//递归快速排序  
void QuickSort(vector &data, int start, int end)
{
	if (start >= end)
		return;
	int index = Partition(data, start, end); //基准的索引
	QuickSort(data, start, index - 1);
	QuickSort(data, index + 1, end);
}

        现在回答,为什么在阿里笔试过程中,自己写出来的快排,感觉没错,但是运行结果不对。主要原因:
void QuickSort(vector& data, int start, int end) //快速排序
int Partition(vector& data, int start, int end) //Partition函数

写成了

void QuickSort(vector data, int start, int end) //快速排序
int Partition(vector data, int start, int end) //Partition函数

运行结果:

在线编程——排序算法总结_第9张图片

本质原因为:vector的传参方式理解不透彻,详见https://www.cnblogs.com/xiaoxi666/p/6843211.html。

为了在面试过程中快速写出代码,如果允许写python代码,则为:

# 快速排序
def quick_sort(array, l, r):
    if l < r:
        q = partition(array, l, r)
        quick_sort(array, l, q - 1)
        quick_sort(array, q + 1, r)

def partition(array, l, r):
    x = array[r]
    i = l - 1
    for j in range(l, r):
        if array[j] <= x:
            i += 1
            array[i], array[j] = array[j], array[i]
    array[i + 1], array[r] = array[r], array[i + 1]
    return i + 1

if __name__=="__main__":
    arr = [int(i) for i in input().split()] # 输入待排序列
    quick_sort(arr,0,len(arr)-1)            # 快速排序
    print(" ".join(str(i) for i in arr))    # 输出排序结果

在线编程——排序算法总结_第10张图片

你可能感兴趣的:(基础常识,python编程,C++编程,在线编程)