几种排序算法的比较

0 写在前面的是一些书上抄来的定义:
  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
  • 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
  • 内排序:所有排序操作都在内存中完成;
  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
  • 时间复杂度: 一个算法执行所耗费的时间。
  • 空间复杂度:运行完一个程序所需内存的大小。

0.1 不知道从哪搞了一张图,但是暂时只讨论几种内排序。考试要考的各种复杂度,想必会有不少强背的吧。就考前三种还记不住拖出去打死                      值得一提的是,矩阵快速转置就是一种外排序

0.2 接下来介绍的可以分成四类,同样是不知道从哪搞的图。

1.选择排序

什么丢进去都有O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。从思路上来说,大概这个是最符合人脑排序的思路了。
它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
void selectSort(int arr[], int max)
{
    int  temp = -1, needsSwap = 0;
    for (int i = 0; i < max; i++)
	{
	    temp = i;
	    needsSwap = 0;
	    for (int j = i; j < max; j++)
	    {
	       if (arr[j] < arr[temp])
		{
		 temp = j;
		 needsSwap = 1;
	        }
	    }
	    if(needsSwap)
	      Swap(&arr[i], &arr[temp]);
	}
}

2.冒泡排序

这是一个极其简洁的排序思路。它持续地遍历数组,一次比较两个元素,如果它们的顺序错误就把它们交换过来,直到没有元素再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

void bubbleSort(int arr[], int max)
{
    int issSwap=0;
    for(int i=0;i<max;i++)
       {
        isSwap=0;
        for(int j=0;j<max-i-1;j++)
           if(arr[j]>arr[j+1])
             {
               swap(arr[j],arr[j+1]);
               isSwap=1;
             }
         if(isSwap==0)
             break;
       }
}

3.插入排序

这是一个类似整理扑克牌的排序手段。我们从某一个元素开始,循环选择前面的元素和它比较并且决定插入点,插入前将后续元素移动以腾出空间。因此在顺序表中进行插入排序可能会导致惊人的数据移动量,带来令人不满的时间复杂度。

void insertSort(int arr[], int max)
{
    for(int i=0;i<max-1;i++)
       {
         int current=arr[i+1];
         int index=i;
         while(index>=0 && arr[index]>current)
             {
                arr[index+1]=arr[index];
                index--;
             }
         arr[index+1]=current;
        }
}
爆破性的while循环导致了大量资源被花费在了移动数组上

4.希尔排序

历史的进程是不可阻挡的,Donald Shell 于1959年改进了插入排序算法,使其成功冲破O(n2)的诅咒。它与插入排序的不同之处在于,它会优先比较距离较远的元素。

希尔排序又叫缩小增量排序,其把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。刚才提到的移动数组的爆炸性while循环,此时被巧妙的拆分了。

先给出代码,接着再仔细分析

void shellSort(int arr[], int max)
{
    int gap = max;
    int i, j, k;
    do
    {
      gap = gap / 3 + 1;
      for (i = 0; i < gap; i++)
	{
          for (j = i + gap; j < max; j += gap)
	  {
             if (arr[j] < arr[j - gap])
	      {
                int temp = arr[j];
                for (k = j - gap; k >= 0 && temp < arr[k]; k -= gap)
                  {
                    arr[k + gap] = arr[k];
                  }
                arr[k + gap] = temp;
               }
           }
	}
   } while (gap > 1);
}

值得一提的是,希尔本人选取的是gap= max/2 但事实上这样会导致出现重复劳动导致效率降低。间隔序列中的数字互质通常被认为很重要:也就是说,除了1之外它们没有公约数。这个约束条件使每一趟排序更有可能保持前一趟排序已排好的效果。希尔最初以N/2为间隔的低效性就是归咎于它没有遵守这个准则。无论选择怎样的分隔,其计算过程都不应该过于复杂而拖累算法本身。不过这种算法的时间复杂度的数学解因为过于困难,还没有被求出来。

5.归并排序

警告:“想要理解递归,首先要理解递归”

归并排序采用分治法(Divide and Conquer),其是一种稳定的排序方法。通过将将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。这种做法会消耗额外的存储空间(临时数组),但能够显著降低时间复杂度。

可以发现有类似二叉树的结构,因此可以用递归实现。当然,也可以转化成迭代的形式。

用递归将数组分割,先分割左半部分再分右半(只是习惯而已),然后每个左右数组一层一层地 排序 并返回。

 

    
void merge(int arr[], int left, int mid, int right, int temp[])
{
	int l = left, md = mid, mdd = mid + 1, r = right, k = 0, i = 0;
	while (l <= md && mdd <= r)
	{
		if (arr[l] <= arr[mdd])
			temp[k++] = arr[l++];
		else
			temp[k++] = arr[mdd++];
	}
	while (l <= md)
	{
		temp[k++] = arr[l++];
	}
	while (mdd <= r)
	{
		temp[k++] = arr[mdd++];
	}
	for (i = 0; i < k; i++)
		arr[left + i] = temp[i];

}

void divide(int arr[], int left, int right, int temp[])
{
	if (left < right)
	{
		int mid = (left + right) / 2;
		divide(arr, left, mid, temp);
		divide(arr, mid + 1, right, temp);
		merge(arr, left, mid, right, temp);
	}
}

void mergeSort(int arr[], int left, int right,int len)
{
	int *temp=(int*)malloc(sizeof(int)*len);
	divide(arr, left, right-1, temp);
        free(temp);
}

想理解又理解不了很令人恼怒吧(装傻.jpg)?亲可以先去复习一下递归的概念呢(装傻.jpg)

 

6.快速排序

快速排序是冒泡排序的改进版,也是现有最好的一种内排序,有着最好的时间复杂度。在一些非常非常极端的情况下,它会退化成冒泡排序。它是一种不稳定的排序算法。

说白了,快速排序可以理解成为某一个指定元素寻找插入位置,这个位置的左右两个的元素要么全部大于它,要么全部小于它。
随后以两个指针重叠的地方为基准,将数组分开继续调用快速排序,直到区间只有一个元素后返回,就得到了完整的排序。
void QuickSort(int* arr, int l, int r)
{
    int left = l;
    int right = r;
    int key = arr[left];
    if (left >= right)  //此时排序结束了
    {
        return;
    }
    while (left < right)  
    {
        while (left < right && key <= arr[right]) { --right; } if (key > arr[right])
        {
            arr[left] = arr[right]; 
            ++left;
        }
        while (left < right && key >= arr[left])
        {
            ++left;  
        }
        if (key < arr[left])
        {
            arr[right] = arr[left];  
            --right;
        }
    }
    arr[left] = key; 
    QuickSort(arr, l, left - 1);  
    QuickSort(arr, left + 1, r);
}

但是,快速排序有一个隐患就是key的选择

比如我们对一个数组{ 9,8,7,6,5 }想通过快速排序来变成从小到大的排序。如果还是选择以第一个元素为枢纽元的话,快速排序就变成选择排序了。

所以,在实际应用中如果数据都是是随机数据,那么选择第几个做key都没有什么不妥。因为这个本来就是看“人品”的。

但是,如果是对于一些比较有规律的数据,我们的“人品”可能就不会太好的。所以常见的有两种选择策略:

一种是使用随机数来做选择

另一种是取数组中的第一个,最后一个和中间一个,选择数值介于最大和最小之间的

这一种又叫做“三数中值分割法”。理论上,这两种选择策略还是可能很悲剧的。但概率要小太多了。


7.堆排序

开不开心,惊不惊讶?!我居然写过这个!

其实是文字不好说明白所以才不在这里列的

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注