排序算法浅析(一)比较排序算法

        比较排序算法,就是通过对不同元素的直接比较,确定大小关系,进而将一组数据排序的方法。这种比较可以是相邻元素间的,也可是是不同元素间的。我们通常遇到的如快速排序算法、堆排序算法、冒泡排序等,都是比较排序算法。

  下面我将对常见的这几种比较排序算法的算法思想进行简单介绍,并附上实现代码。

  一、选择排序算法

  遍历一遍数组,选择其中国最小的元素,放在数组的最左边;在剩下的元素中,继续寻找最小的元素,直到剩余元素为零。选择排序算法的稳定性比较特殊,如果待交换位置与遍历方向相一致,则稳定;如果不相一致,则不稳定。例如:待交换的元素放到最左边,你从右向左遍历选择,相同元素m1、m2,会将m2交换到m1前面。        

void SelectSort(int* iArray,int size)
{
	for( int i = 0; i < size; i++)
	{
		int smallest = iArray[i];	//记录最小元素
		int k = i;	//记录最小元素的位置
		for( int j = i + 1; j < size; j++)
		{
			if( iArray[j] < smallest)
			{
				smallest = iArray[j];
				k = j;
			}
		}
		swap(iArray+i,iArray+k);	//交换第i个与第k个元素
	}
}

      

  二、冒泡排序算法

  冒泡排序算法与选择排序比较类似,也是对所以元素遍历比较,但它不是将最小元素与队首元素交换,而是将相邻的两个元素交换。这样一趟遍历后,最大的元素就能一路交换到队尾。在整个遍历过程,元素就像一个个水泡一样,慢慢的跑到了队尾,所以叫做冒泡排序。因为相互交换的元素都是相邻的元素,不是跳跃性的移动的,所以冒泡排序是一种稳定的排序算法。

void BubbleSort(int* iArray,int size)
{
	for( int i = size ; i > 0 ;i--)
	{
		for( int j = 0; j < i - 1; j++)
		{
			if( iArray[j + 1] < iArray[j])
				swap(iArray + j +1,iArray + j);
		}
	}
}

 

 三、插入排序算法

  插入排序的算法思想与抓牌时扑克牌的插入顺序,原来手上的牌已经有序,抓到新的扑克牌后,与原来的进行比较,遇到比它大的牌后就停下来,将它插入。对于代码实现,开始时数组中的元素已经有序,新插入的元素与其前面的元素进行比较,若新元素小于它前面的元素,就与再前面的元素比较,直到新元素大于前面的元素。插入排序也是稳定的。

void InsertSort(int* iArray,int size)
{	
	for( int i = 1; i < size; i++)
	{
		int key = iArray[i];
		//对i前面的元素进行比较,当前值大于key,则移动到后面
		int j = i -1;
		while( j >= 0 && key < iArray[j] )
		{
			iArray[j+1] = iArray[j];
			j--;
		}
		//当前值不大于key了,循环停下来,将key插入当前值后面
		iArray[j+1] = key;
	}
}

 

 四、希尔排序

  希尔排序是插入排序的一种改进算法,它先将数组分成小的子序列,然后对这些小的子序列进行插入排序。然后再次将数组分成稍微大一些的子序列,进行插入排序,重复几次操作后,数组基本有序,再对整个数组进行一次插入排序。第一趟排序时,子序列中各元素的间隔比较大,逐渐缩小间隔,子序列的规模会越来越大。

  希尔排序中的这个间隔的选择问题涉及到一个数学上尚未解决的问题。一般的,我们取第k次的间隔间隔dlta[k] = 2t-k+1+ 1,其中k从1开始选取,t是选取间隔的数目,一般取t = log2(n+1)。此时的时间复杂度大约为O(n3/2)。因为是跳跃的交换数据,所以希尔排序是不稳定的。

void ShellSort(int* iArray,int size)
{
	int t = int(log(float(size + 1))/log(float(2)));	//希尔插入的次数
	int k =0;
	int dlta = (int)pow(float(2),t-k) - 1;	
	while( dlta >= 1 )
	{
		ShellInsert(iArray,size,dlta);
		k++;
		dlta = (int)pow(float(2),t-k) - 1;
	}
}

//dlta表示间隔
void ShellInsert(int* iArray,int size,int dlta)
{
	//将数组分割成dlta个子序列
	for( int i = 0; i < dlta; i++)
	{
		//对每个子序列,采用插入排序
		for( int k = i + dlta; k < size; k = k + dlta)
		{
			int key = iArray[k];
			int j = k - dlta;
			while( j >= 0 && key < iArray[j] )
			{
				iArray[j + dlta] = iArray[j];
				j = j - dlta;
			}
			iArray[j + dlta] = key;
		}
		
	}
}


 

 五、并归排序

  并归排序采用的是分治法的思想,就是将一个大的数组分成两个小的数组,然后分别对着两个小的数组进行排序,待两个数组有序后,再将两个子数组并归成一个数组。该算法是稳定的。

#define FINITE            0x0FFFFFFF

void Merge(int* iArray,int StartPos,int MidPos,int EndPos);
void MSort(int* iArray,int StartPos,int EndPos);

void MergeSort(int* iArray,int size)
{
	MSort(iArray,0,size-1);
}
//并归排序采用分治法思想
void MSort(int* iArray,int StartPos,int EndPos)
{
	if( StartPos < EndPos)
	{
		int MidPos = (StartPos + EndPos)/2;
		MSort(iArray,StartPos,MidPos );
		MSort(iArray,MidPos + 1,EndPos);
		Merge(iArray,StartPos,MidPos,EndPos);
	}
}

//StartPos第一个数组开始位置,MidPos第一个数组结束位置,EndPos为第二个数组结束位置
void Merge(int* iArray,int StartPos,int MidPos,int EndPos)
{
	int Size1 = MidPos - StartPos + 1;	//第一个数组长度
	int Size2 = EndPos - MidPos;	//第二个数组长度
	//生成两个数组,用来临时保存数据
	int* Array1 = (int*)malloc((Size1 +1) * sizeof(int));
	int* Array2 = (int*)malloc((Size2 +1) * sizeof(int));

	//赋值
	int i = 0;
	for( ; i < Size1 ; i++)
		Array1[i] = iArray[StartPos + i];
	Array1[i] = FINITE;	//标记终点
	
	int j = 0;
	for( ; j < Size2 ; j++)
		Array2[j] = iArray[MidPos + 1 + j];
	Array2[j] = FINITE;	//标记终点

	//将生成的两个数组中的数据拷贝到原始数组中
	int CurPos = StartPos;
	int m=0,n=0;	//指示数组中元素的位置
	while( CurPos<= EndPos)
	{
		if( Array1[m] < Array2[n])
		{
			iArray[CurPos] = Array1[m];
			m++;
		}
		else
		{
			iArray[CurPos] = Array2[n];
			n++;
		}
		CurPos++;
	}
	free(Array1);
	free(Array2);
}


        在该代码中,先将原始的数据拷贝到另外的两个数组中,并归时再拷贝回来,所以会有O(n)的空间复杂度。在新数组的末尾添加一个“∞”是一个小技巧,相当与添加了一个标兵,这样比较到数组尾部的时候就会自动停下来了,而不用每次都判断是否到达数组尾部。当两个子数组都到达尾部时,就会停止循环了。

  六、快速排序

  面试中最容易考的排序算法了。其实快排的思想很简单,也是一种分治,先以第一个元素作为key,将小于它的都放到左边大于它的放到右边,然后分别对左边的子数组和右边的子数组调用快速排序就好了。所以问题的关键是Partion算法,也就是将元素按key排布的算法。我喜欢把这个算法看成是填“洞”算法,取出第一个元素放入key中后,第一个元素的位置就出现了一个“洞”。然后,从右边开始遍历,找到小于key的元素,这个元素本来应该是在左边的,所以把它填到左边的“洞”中,这样右边刚刚取出元素的位置,又有了一个“洞”,在从左边取个大于key的元素放到右边的“洞”中,这样一直循环下去,知道左右两边的指针相遇。将key放到,当前指针的位置就好啦。该算法不稳定。

void QuickSort (int* StartPos, int* EndPos)
{
	if( StartPos < EndPos )
	{
		//找到中间位置
		int* mid = Partion(StartPos,EndPos);
		QuickSort(StartPos,mid - 1);
		QuickSort(mid + 1,EndPos);
	}
}
//对StartPos和EndPos之间数据进行排序,将大于StartPos[0]的数据都放在StartPos[0]左边,小于它的放在右边
int* Partion(int* StartPos, int* EndPos)
{
	//key数据,所以数据都跟它比较
	int key = StartPos[0];	
	//记录“洞”的位置,开始时最左边有一个洞,右边的数据移动到洞中
	int* HolePos = StartPos;	

	//定义左右移动的指针
	int* low  = StartPos;  //	左
	int* high = EndPos;		//	右
	while( low < high )
	{
		while( *high <= key && low < high ) high--;
		//while停下时说明,左边存在大于key的数据,将其移动到右边的“洞”中
		*HolePos = *high;
		HolePos =  high;
		while( *low  >= key && low < high ) low ++;
		//while停下时说明,右边存在大于key的数据,将其移动到左边的“洞”中
		*HolePos = *low;
		HolePos =  low;
		//交换过数据后,while循环肯定能通过,high和low会自加自减
	}
	//最后 low == high时,说明已经找到key的位置了,就在最后一次交换的“洞”中
	*HolePos = key;
	return HolePos;
}

        编写代码时,有两点注意:1、low的初始值要从key开始 2、比较时,条件是大于等于。这两个注意不是必须的,但是这样写我觉得比较容易理解,而且代码容易实现。

  七、堆排序

  堆排序的思想来源于一种叫做堆的数据结构,堆是一棵完全二叉树。堆可以有大根堆和小根堆,大根堆的意思就是,父节点的值大于两个子节点的值;小根堆,父节点的值小于子节点的值。对于大根堆,堆顶元素一定是数组中最大的元素。根据这一性质,发展出了堆排序,该算法不稳定。

  在堆排序算法中,一个子算法叫做堆的性质保持算法(Heapify),就是当左右子树都满足堆性质时,父节点的值小于了左右子树,这时要对堆进行调整,将左右子节点中比较大的元素交换到父节点上,让后再对交换的子节点进行Heapify。

void MaxHeapify(Heap* pHeap,int i)
{
	int left = LeftChild(i);
	int right = RightChild(i);
	int largest = i;
	if( left < pHeap->HeapSize && pHeap->iArray[left] > pHeap->iArray[i])
		largest = left;
	if( right < pHeap->HeapSize && pHeap->iArray[right] > pHeap->iArray[largest])
		largest = right;
	//如果左右子树大于父树,则交换
	if( largest != i)
	{
		HeapSwap(pHeap,i,largest);
		//对子树递归调用
		MaxHeapify(pHeap,largest);
	}	
}


        在数组中,左右子树的求法比较简单

int LeftChild( int i )
{
	return 2*(i+1)-1; 
}
int RightChild(int i)
{
	return 2*(i+1)+1-1;
}


        根据Heapify性质,可以将构建一个大根堆,从第一个非叶子节点开始,调用Heapify函数。为什么要从第一个非叶子节点开始呢,是为了满足使用Heapify的前提条件,就是子节点必须满足堆性质,叶子节点肯定能满足这一个条件。

void BuidHeap(Heap* pHeap)
{
	//从后向前,找到第一个非叶子树
	int noChild = (pHeap->HeapSize)/2 - 1;
	for( int i = noChild ; i >= 0; i--)
	{
		MaxHeapify(pHeap,i);
	}
}


        建立大根堆后,将堆顶元素取出,与队尾元素进行交换,然后将堆的大小减一。这样,最大的元素就不在堆中了,而取出最后一个元素,对左右子树的堆性质不造成任何影响,然后再调用Heapify,再取出堆顶元素…………这样一直循环下去,直到取出所以元素。

typedef struct _Heap{
	int* iArray;
	int HeapSize;
}Heap;

void HeapSort(int* iArray,int size)
{
	Heap* pHeap = (Heap*)new(Heap);
	pHeap->iArray = iArray;
	pHeap->HeapSize = size;
	BuidHeap(pHeap);
	for(int i = pHeap->HeapSize - 1; i > 0; i--)
	{
		//将堆顶元素与最后一个元素交换,
		HeapSwap(pHeap,0,i);
		//最大的元素从堆中被取出
		pHeap->HeapSize--;
		//对剩下的堆,保存堆性质
		MaxHeapify(pHeap,0);
	}
}


         总结:以上是几种常见的比较算法,只是简单的介绍了下,主要是害怕自己忘记,写下来加强记忆。

posted @ 2012-05-13 18:04  DylanTsou  阅读(374)  评论(0编辑  收藏  举报