许多计算机科学家认为,排序算法是算法学习中最基本的问题。那么我们就从排序这类算法开始,去看看算法究竟是什么。
排序算法解决的是一类什么具体问题呢?
输入:n个数的序列<a1,a2,a3,...,an>
输出:输入序列的一个重排<a'1,a'2,a'3,...,a'n>,使得a'1<=a'2<=a'3<=...<=a'n
输入序列通常是一个n元数组,但也可能由其他形式来表示,如链表。
对于这个问题,我们可以很容易联想到日常生活中的许多例子。那么我们解决这个问题的第一个思路便可以从日常生活中获得。
插入排序,这是一个对少量元素进行排序的有效算法。其工作机理和很多人打牌时,整理手中牌时的做法差不多。在开始摸牌时,我们的左手是空的,牌面朝下的放在桌上。接着,一次从桌上摸起一张牌,并将它插入到左手一把牌中的正确位置上。为了找到这张牌合适的位置,要将它与手中已有的每一张牌从右向左地进行比较。无论在什么时候,左手中的牌都是排好序的,而这些牌原先都是桌上那副牌里最顶上的一些牌。
//主方法
INSERTION-SORT(A)
//从桌面上一次次取牌
for j ← 2 to length[A]
do
//取一张牌到右手key
key ← A[j]
//眼睛注意的位置(将要比较的位置)
i ← j – 1
//开始寻找合适的位置
while i > 0 and A[i] > key
do
//微调左手中的已经排好顺序的牌
A[i + 1] ← A[i]
//移动眼睛,转换比较的对象
i ← i – 1
//将右手中的牌放在左手牌中合适的位置上
A[i + 1] ← key
大概的思路已经在脑子中形成了,剩下的是简单的工作:将思路转化为实实在在的代码。在这里我用java编写了一个静态方法。关于这个算法的具体证明过程详见《Introduction to Algorithms》.
*InsertionSort
*The time limit of this sort is O(n^2)
*<strong>O(n^2)</strong>
*@param element will be sorted
*在这段代码中使用了java的范型以及一个对象接口,详细情况可以参见java相关教材
*/
public static < Type extends Comparable > void insertionSort(Type element[]) {
for(int j=1;j<element.length;j++){
Type key = element[j];
int i = j-1;
while( (i>=0)&&(element[i].compareTo(key)>0)){
element[i+1] = element[i];
i--;
}
element[i+1] = key;
}
}
如同大家打牌一样,很多人熟能生巧整理手中的牌时又准又快,而有些人却费时费力。那么一个算法也是友好有坏,在这里我只给出相关的评价指数,至于具体规定可参见相关书籍。
这个算法的时间复杂度为O(n^2),空间复杂度为O(n).关于“O”符号可以在微积分类书籍上找到更为详尽的解释。
科学技术不断进步,很多新的算法应运而生。在这里再介绍一种排序算法。它在时间复杂度上有了具大提高,空间上却付出了两倍的代价。
合并排序,一种利用了“分治策略”的排序方法。分治策略:将原问题划分成n个规模较小而结构简单与原问题相似的子问题;递归地解决这些子问题,然后再合并其结果,就得到原问题的解。排序算法就是借助了这种思想,本质是:从中间把数组分成元素个数尽量相等的两半,分别对他们进行排序,然后再合并两个有序表。整个问题最困难的地方便是合并两个有序表,在这里我们着重看看这个过程。
再举打牌这个例子,假设有两堆牌正面朝上地放在桌上,每一堆都是已经排好顺序的,最小的牌在最上面。我们希望把这两堆牌合并成一个排好序的输出堆,面朝下地放在桌上。基本步骤包括在面朝上的两堆牌中,选取顶上两张牌中较小的一张,将其取出后面朝下地放入输出堆中。重复这个步骤,直到某一堆为空为止。
*Merge used in the mergeSort
*<strong>O(n)</strong>
*@param element Type[] the elements to be merged
*@param p int the begin of the first elements
*@param q int the end of the first elements
*@param r int the end of the second elements
*/
private static <Type extends Comparable> void merge(Type element[],int p,int q,int r){
//确定两堆牌的起始位置
int nl = q-p+1;
int nr = r-q;
//新建两个输入堆用于存放待组合牌
Type[] lElement,rElement;
lElement = (Type[])java.lang.reflect.Array.newInstance(element.getClass().getComponentType(),nl);
rElement = (Type[])java.lang.reflect.Array.newInstance(element.getClass().getComponentType(),nr);
//做记录处理
for(int i=0;i<nl;i++){
lElement[i] = element[p+i];
}
for(int i=0;i<nr;i++){
rElement[i] = element[q+i+1];
}
//开始取牌
int i=0,j=0;
int k = p;
//逐个比较,一直到一个堆为空
while((i<nl)&&(j<nr)){
if(lElement[i].compareTo(rElement[j])<=0){
element[k] = lElement[i];
i++;
}else{
element[k] = rElement[j];
j++;
}
k++;
}
//处理某一堆中 剩下的牌
while(i<nl){
element[k] = lElement[i];
i++;
k++;
}
while(j<nr){
element[k] = rElement[j];
j++;
k++;
}
}
这个算法的时间复杂度为O(n log2 n),空间复杂度为O(2n)=O(n).
至此两个算法的介绍结束,我们将这类算法扩大化,从中取出根本的东西。
逆序对
逆序对是指在一个元素序列中,按照一定的大小比较方法,序列中任两个元素大小顺序颠倒的组合。
设A[1..n]是一个包含n个不同数的数组.如果在i<j的情况下,有A[i]>A[j],则(i,j)就称为A中的一个逆序对.
对于一个给定待排序序列,其中的逆序对的发现与还原正是排序所要解决的事情.排序过程中一般是先发现逆序对再将其还原,由于这些让我们联想到了排序性能提高的一些方法.
1.一次还原操作,使得多组逆序对还原.
eg: 共有21对 共有10对
7,6,5,4,3,2,1--(1与7互换)-->1,6,5,4,3,2,7
一次还原处理了11对
2.一次对换操作后,尽量不要或者少产生额外的逆序对
eg: 共有16对 共有11对
7,6,5,1,3,2,4--(4与7互换)-->4,6,5,1,3,2,7
一次还原处理了5对,另外又多产生了(4,1)一对
在我观察的比较类排序中,快速排序体现了1却缺失了2;合并排序恰恰相反体现了2却缺失了1.
关于逆序对的相关统计算法代码参见
* Copyright (C) 2000-2007 Wang Pengcheng <wpc0000@163.com>
* Licensed to the Wang Pengcheng under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The LGPL licenses this file to You under the GNU Lesser General Public
* Licence, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.gnu.org/licenses/lgpl.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mypackage.algorithm.unit02;
public class Inversion {
private static int tot;
/**
*InsertionSort
*The time limit of this sort is O(n^2)
*<strong>O(n^2)</strong>
*@param element will be sorted
*/
private static <Type extends Comparable> void insertionSort(Type element[]){
for(int j=1;j<element.length;j++){
Type key = element[j];
int i = j-1;
while( (i>=0)&&(element[i].compareTo(key)>0)){
element[i+1] = element[i];
tot++;
i--;
}
element[i+1] = key;
}
}
/**
*Merge used in the mergeSort
*<strong>O(n)</strong>
*@param element Type[] the elements to be merged
*@param p int the begin of the first elements
*@param q int the end of the first elements
*@param r int the end of the second elements
*/
private static <Type extends Comparable> void merge(Type element[],int p,int q,int r){
int nl = q-p+1;
int nr = r-q;
Type[] lElement,rElement;
lElement = (Type[])java.lang.reflect.Array.newInstance(element.getClass().getComponentType(),nl);
rElement = (Type[])java.lang.reflect.Array.newInstance(element.getClass().getComponentType(),nr);
for(int i=0;i<nl;i++){
lElement[i] = element[p+i];
}
for(int i=0;i<nr;i++){
rElement[i] = element[q+i+1];
}
int i=0,j=0;
int k = p;
while((i<nl)&&(j<nr)){
if(lElement[i].compareTo(rElement[j])<=0){
element[k] = lElement[i];
i++;
}else{
element[k] = rElement[j];
j++;
tot+=nl-i;
}
k++;
}
while(i<nl){
element[k] = lElement[i];
i++;
k++;
}
while(j<nr){
element[k] = rElement[j];
j++;
k++;
}
}
/**
*mergeSort
*Sort the elements by the compareTo method
*<strong>O(nlgn)</strong>
*@param element Type[] that will be sorted
*@param p the begin of the list will be sorted
*@param r the end of the list will be sorted
*/
private static <Type extends Comparable> void mergeSort(Type element[],int p,int r){
if(p<r){
int q = (p+r)/2;
mergeSort(element,p,q);
mergeSort(element,q+1,r);
merge(element,p,q,r);
}
}
private static <Type extends Comparable> void mergeSort(Type element[]){
mergeSort(element,0,element.length-1);
}
/**
* Count the inversion number by O(nlgn)
* @param <Type> inversion number type
* @param element inversion number list
* @return count number of inversion
*/
public static <Type extends Comparable> int countMerge(Type element[]){
tot=0;
mergeSort(element.clone());
return tot;
}
/**
* Count the inversion number by O(n^2)
* @param <Type> inversion number type
* @param element inversion number list
* @return count number of inversion
*/
public static <Type extends Comparable> int countInsertion(Type element[]){
tot=0;
insertionSort(element.clone());
return tot;
}
/**
* @param args
*/
public static void main(String[] args) {
Integer[] a = {4,6,5,1,3,2,7};
System.out.println(Inversion.countMerge(a));
}
}
最后让我们牢记:
摘《李开复:算法的力量》算法并不局限于计算机和网络
举一个计算机领域外的例子:在高能物理研究方面,很多实验每秒钟都能几个TB的数据量。但因为处理能力和存储能力的不足,科学家不得不把绝大部分未经处理 的数据丢弃掉。可大家要知道,新元素的信息很有可能就藏在我们来不及处理的数据里面。同样的,在其他任何领域里,算法可以改变人类的生活。例如人类基因的 研究,就可能因为算法而发明新的医疗方式。在国家安全领域,有效的算法可能避免下一个911的发生。在气象方面,算法可以更好地预测未来天灾的发生,以拯 救生命。
所以,如果你把计算机的发展放到应用和数据飞速增长的大环境下,你一定会发现;算法的重要性不是在日益减小,而是在日益加强。
谢谢大家。
参考书籍
《Inroduction to Algorithms》Thomas H.Cormen,Charles E.Leiserson,Ronald L. Rivest,Clifford Stein.机械工业出版社
《数据结构》殷人昆,陶永雷,谢若阳,盛绚华 清华大学出版社
《算法艺术与信息学竞赛》刘汝佳,黄亮 清华大学出版社
《李开复:算法的力量》http://www.ieee.org.cn/dispbbs.asp?boardID=60&ID=31651