排序算法总结(c#版)

来源:互联网 发布:电工理论与新技术 知乎 编辑:程序博客网 时间:2024/05/16 19:29

算法质量的衡量标准:

1:时间复杂度:分析关键字比较次数和记录的移动次数;

2:空间复杂度:需要的辅助内存;

3:稳定性:相同的关键字计算后,次序是否不变。

排序的分类

1:选择排序(直接选择排序、堆排序)

2:交换排序(冒泡排序、快速排序)

3:插入排序(直接插入排序、折半插入排序、Shell排序)

4:归并排序

5:捅式排序

6:基数排序

1:直接选择排序

思路:

第一次从R[0]~R[n-1]中选取最小值,与R[0]交换,第二次从R{1}~R[n-1]中选取最小值,与R[1]交换,...., 第i次从R[i-1]~R[n-1]中选取最小值,与R[i-1]交换,.....,第n-1次从R[n-2]~R[n-1]中选取最小值,与R[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列。

效率分析:

在直接选择排序中,共需要进行n-1次选择和交换,每次选择需要进行 n-i 次比较 (1<=i<=n-1),而每次交换最多需要3次移动,因此,总的比较次数C=1/2(n*n - n),

总的移动次数 3(n-1).由此可知,直接选择排序的时间复杂度为 O(n2) (n的平方),所以当记录占用字节数较多时,通常比直接插入排序的执行速度快些。

由于在直接选择排序中存在着不相邻元素之间的互换,因此,直接选择排序是一种不稳定的排序方法。

 

代码:

private static void SelectSort(int[] data)        {            int arrayLength = data.Length;            //依次进行n-1次比较,第i次比较将第i大的值选出放在i的位置上            for (int i = 0; i < arrayLength - 1; i++)            {                //保留最小值的索引                int minIndex = i;                //将第i个数据和它后面的数据比较                for (int j = i + 1; j < arrayLength; j++)                {                     if (data[minIndex] > data[j])                    {                        minIndex = j;                    }                }                //交换数据                if (minIndex != i)                {                    int temp = data[i];                    data[i] = data[minIndex];                    data[minIndex] = temp;                }            }        }


 

 

2:堆排序

 

思路:

堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。

用大根堆排序的基本思想

  ① 先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区

  ② 再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key

  ③由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。

  ……

  直到无序区只有一个元素为止。

 

效率分析:

排序的时间,主要由建立初始]堆和反复重建堆这两部分的时间开销构成,它们均是通过调用Heapify实现的。

  堆排序的最坏时间复杂度为O(nlogn)。堆序的平均性能较接近于最坏性能。

  由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。

  堆排序是就地排序,辅助空间为O(1),

  它是不稳定的排序方法。

 

代码:

private static void HeapSort(int[] data)        {            int arrayLength = data.Length;            //建堆            for (int i = 0; i < arrayLength - 1; i++)            {                 //建堆                BuildMaxdHeap(data, arrayLength - 1 - i);                //交换堆顶和最后一个元素                Swap(data, 0, arrayLength - 1 - i);             }        }        private static void BuildMaxdHeap(int[] data, int lastIndex)        {            //从最后一个节点的父节点开始            for (int i = (lastIndex - 1) / 2; i >= 0; i--)            {                 //当前位置                int k = i;                //如果当前k节点存在子节点                while (k * 2 + 1 <= lastIndex)                {                    int biggerIndex = k * 2 + 1;                    //判断右子节点是否存在                    if (biggerIndex < lastIndex)                    {                         if (data[biggerIndex] < data[biggerIndex + 1])                            biggerIndex++;                    }                    if (data[k] < data[biggerIndex])                    {                         Swap(data, k, biggerIndex);                        k = biggerIndex;                    }                    else                    {                        break;                    }                }            }        }        private static void Swap(int[] data, int i, int j)        {            int temp = data[i];            data[i] = data[j];            data[j] = temp;        }

 

3:冒泡排序

 

思路:

次比较相邻的两个数,将小数放在前面,大数放在后面。即在第一趟:首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。至此第一趟结束,将最大的数放到了最后。在第二趟:仍从第一对数开始比较(因为可能由于第2个数和第3个数的交换,使得第1个数不再小于第2个数),将小数放前,大数放后,一直比较到倒数第二个数(倒数第一的位置上已经是最大的),第二趟结束,在倒数第二的位置上得到一个新的最大数(其实在整个数列中是第二大的数)。如此下去,重复以上过程,直至最终完成排序。

 

效率分析:

若记录序列的初始状态为"正序",则冒泡排序过程只需进行一趟排序,在排序过程中只需进行n-1次比较,且不移动记录;反之,若记录序列的初始状态为"逆序",则需进行n(n-1)/2次比较和记录移动。因此冒泡排序总的时间复杂度为O(n*n)。

 

代码:

private static void BubbleSort(int[] data)        {            int arrayLength = data.Length;            for (int i = 0; i < arrayLength - 1; i++)            {                bool flag = false;                for (int j = 0; j < arrayLength - 1 - i; j++)                {                    if (data[j] > data[j + 1])                    {                        int temp = data[j];                        data[j] = data[j + 1];                        data[j + 1] = temp;                        flag = true;                    }                }                if (!flag)                {                    break;                }            }        }

 

4:快速排序

思路:

1)设置两个变量I、J,排序开始的时候:I=0,J=N-1;

  2)以第一个数组元素作为关键数据,赋值给key,即 key=A[0];

  3)从J开始向前搜索,即由后开始向前搜索(J=J-1即J--),找到第一个小于key的值A[j],A[j]与A[i]交换;

  4)从I开始向后搜索,即由前开始向后搜索(I=I+1即I++),找到第一个大于key的A[i],A[i]与A[j]交换;

  5)重复第3、4、5步,直到 I=J; (3,4步是在程序中没找到时候j=j-1,i=i+1,直至找到为止。找到并交换的时候i, j指针位置不变。另外当i=j这过程一定正好是i+或j-完成的最后令循环结束。)

 

效率分析:

冒泡排序的一种改进

 

代码:

 

private static void QuickSort(int[] data)        {            //调用子排序            SubSort(data, 0, data.Length - 1);        }        //对data数组中start到end范围内的子序列进行排序        private static void SubSort(int[] data, int start, int end)        {            if (start < end)            {                //第一个元素作为分界值                int middle = data[start];                int i = start;                int j = end + 1;                                while (true)                {                    while (i < end && data[++i] <= middle) ;                    while (j > start && data[--j] >= middle) ;                    if (i < j)                    {                        Swap(data, i, j);                    }                    else                    {                        break;                    }                }                Swap(data, start, j);                //递归进行左子排序                SubSort(data, start, j - 1);                //递归进行右子排序                SubSort(data, j + 1, end);            }        }


5:直接插入排序

思路:

每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。

  第一趟比较前两个数,然后把第二个数按大小插入到有序表中; 第二趟把第三个数据与前两个数从前向后扫描,把第三个数按大小插入到有序表中;依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程。

  直接插入排序属于稳定的排序,最坏时间复杂性为Θ(n^2),空间复杂度为O(1)。

  直接插入排序是由两层嵌套循环组成的。外层循环标识并决定待比较的数值。内层循环为待比较数值确定其最终位置。直接插入排序是将待比较的数值与它的前一个数值进行比较,所以外层循环是从第二个数值开始的。当前一数值比待比较数值大的情况下继续循环比较,直到找到比待比较数值小的并将待比较数值置入其后一位置,结束该次循环

 

效率分析:

速度慢,空间效率高,稳定。

 

代码:

private static void InsertSort(int[] data)        {            for (int i = 1; i < data.Length; i++)            {                //备份data[i]的值                int temp = data[i];                if (data[i] < data[i - 1])                {                    int j = i - 1;                    //通过比较找出要插入的位置,同时将大的数值向右移动                    for (; j >= 0 && data[j] > temp; j--)                    {                        data[j + 1] = data[j];                    }                    //将temp插入合适的位置                    data[j + 1] = temp;                }            }        }


 

6:折半插入排序

 

思路:

折半插入排序(binary insertion sort)是对插入排序算法的一种改进,由于排序算法过程中,就是不断的依次将元素插入前面已排好序的序列中。由于前半部分为已排好序的数列,这样我们不用按顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。

折半插入排序算法的具体操作为:在将一个新元素插入已排好序的数组的过程中,寻找插入点时,将待插入区域的首元素设置为a[low],末元素设置为a[high],则轮比较时将待插入元素与a[m],其中m=(low+high)/2相比较,如果比参考元素小,则选择a[low]到a[m-1]为新的插入区域(即high=m-1),否则选择a[m+1]到a[high]为新的插入区域(即low=m+1),如此直至low<=high不成立,即将此位置之后所有元素后移一位,并将新元素插入a[high+1]。

 

效率分析:

折半插入排序算法是一种稳定的排序算法,比直接插入算法明显减少了关键字之间比较的次数,因此速度比直接插入排序算法快,但记录移动的次数没有变,所以折半插入排序算法的时间复杂度仍然为O(n^2),与直接插入排序算法相同。

 

代码:

private static void BinaryInsertSort(int[] data)        {            for (int i = 1; i < data.Length; i++)            {                //备份data[i]的值                int temp = data[i];                int low = 0;                int high = i - 1;                while (low <= high)                {                    //取low到high中间的索引                    int middle = (low + high) / 2;                    //确定temp在中间值的哪侧                    if (temp > data[middle])                    {                        low = middle + 1;                    }                    else                    {                        high = middle - 1;                    }                }                //将low到i处的所有元素向后整体移一位                for (int j = i; j > low; j--)                {                    data[j] = data[j - 1];                }                //将temp插入合适的位置                data[low] = temp;            }        }


 

7:Shell排序

 

思路:

先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为dl的倍数的记录放在同一个组中。先在各组内进行直接插人排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<;…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。

 

效率分析:

性能由于直接插入排序,不稳定

 

代码:

private static void ShellSort(int[] data)        {            int arrayLength = data.Length;            //可增变量            int h = 1;            while (h <= arrayLength / 3)            {                h = h * 3 + 1;            }            while (h > 0)            {                for (int i = h; i < arrayLength; i++)                {                    //备份当前值                    int temp = data[i];                    if (data[i] < data[i - h])                    {                        int j = i - h;                        //整体向后移动h格                        for (; j >= 0 && data[j] > temp; j -= h)                        {                            data[j + h] = data[j];                        }                        data[j + h] = temp;                    }                }                h = (h - 1) / 3;            }        }

 

8:归并排序

思路:

申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列

  设定两个指针,最初位置分别为两个已经排序序列的起始位置

  比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置

  重复步骤3直到某一指针达到序列尾

  将另一序列剩下的所有元素直接复制到合并序列尾

 

效率分析:

速度仅次于快速排序,但较稳定

 

代码:

private static void MergeSort(int[] data)        {            Sort(data, 0, data.Length - 1);            }        //将left到right范围内的数组进行归并排序        private static void Sort(int[] data, int left, int right)        {            if (left < right)            {                int center = (left + right) / 2;                //对左侧数组递归排序                Sort(data, left, center);                //对右侧数组递归排序                Sort(data, center + 1, right);                //合并                Merge(data, left, center, right);            }        }        //将两个数组进行归并        private static void Merge(int[] data, int left, int center, int right)        {            int[] tempArray = new int[data.Length];            int middle = center + 1;            int third = left;            int temp = left;            while (left <= center && middle <= right)            {                //从两个数组中取出小的放入中间的数组                if (data[left] <= data[middle])                {                    tempArray[third++] = data[left++];                }                else                {                    tempArray[third++] = data[middle++];                }            }            //将余下的依次放入中间数组            while (middle <= right)            {                tempArray[third++] = data[middle++];            }            while (left <= center)            {                tempArray[third++] = data[left++];            }            //将中间数组中的内容复制到原数组            while (temp <= right)            {                data[temp] = tempArray[temp++];            }        }

9:捅式排序

 

思路:

 

效率分析:

 

代码:

private static void BucketSort(int[] data, int min, int max)        {            int arrayLength = data.Length;            int[] temp = new int[arrayLength];                        //记录待排序元素的信息            int[] buckets = new int[max - min];            //计算每个元素在序列中出现的次数            for (int i = 0; i < arrayLength; i++)            {                buckets[data[i] - min]++;            }            //计算“落入”各捅内的元素在有序序列中的位置            for (int i = 1; i < max - min; i++)            {                buckets[i] = buckets[i] + buckets[i - 1];            }                        //将当前数组备份到temp中            data.CopyTo(temp, 0);                        //根据buckets数组中的信息将待排序的各元素让入相应的位置            for (int k = arrayLength - 1; k >= 0; k--)            {                data[--buckets[temp[k] - min]] = temp[k];            }        }

 

10:基数排序

 

思路:

将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。

 

效率分析:

时间效率:设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行链式基数排序的时间复杂度为O(d(n+radix)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(n),共进行d趟分配和收集。 空间效率:需要2*radix个指向队列的辅助空间,以及用于静态链表的n个指针。

 

代码:

/*         * radix 指定关键字拆分的进制,如10为十进制         * d 指定关键字拆分成几个子关键字         */        private static void RadixSort(int[] data, int radix, int d)        {            int arrayLength = data.Length;            int[] temp = new int[arrayLength];            int[] buckets = new int[radix];            //依次从最高位的子关键字对待排数据进行排序            for (int i = 0, rate = 1; i < d; i++)            {                //重置临时数组                Array.Clear(buckets, 0, arrayLength);                //将data数组进行备份                data.CopyTo(temp, 0);                //计算每个待排序数据的子关键字                for (int j = 0; j < arrayLength; j++)                {                    int subKey = (temp[j] / rate) % radix;                    buckets[subKey]++;                }                for (int j = 1; j < radix; j++)                {                    buckets[j] = buckets[j] + buckets[j - 1];                }                //按照子关键字对指定数据进行排序                for (int m = arrayLength - 1; m >= 0; m--)                {                    int subKey = (temp[m] / rate) % radix;                    data[--buckets[subKey]] = temp[m];                }                rate *= radix;            }        }