八大算法总结

直接上个图,这个图应该你见过很多次了。简单的就是说各个算法的优劣对比。这个其实还是要到代码里去看,去理解了害羞

 

排序算法是我们编程中遇到的最多的算法。目前主流的算法有8种。

  平均时间复杂度从高到低依次是:

  冒泡排序(o(n2)),选择排序(o(n2)),插入排序(o(n2)),堆排序(o(nlogn)),

  归并排序(o(nlogn)),快速排序(o(nlogn)), 希尔排序(o(n1.25)),基数排序(o(n))

 

算法1:直接插入排序

第一步:将第一个待排序序列的元素看成一个有序序列,把第二个到后面的元素当成是未排序序列

第二步:从头到尾依次扫描未排序序列,将扫描到的每一个元素插入有序序列适当的位置(如出现相等放在后面即可)

这也有国外大学的做的模拟插入排序的过程动画:https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html

 public static void InsertionSort()
        {
            //直接插入1
            int[] nums1 = new[] { 6, 5, 3, 1, 8, 7, 2, 4 };
            for (int i = 0; i < nums1.Length; i++)
            {
                int temp = nums1[i];//记录需要排序的数字(提取未排序序列中的数字)
                for (int j = i; j > 0 && nums1[j] < nums1[j - 1]; j--)//从后往前遍历,需找到合适的数字进行交换(放入到已排序序列中的数字)
                {
                    nums1[j] = nums1[j - 1];
                    nums1[j - 1] = temp;
                }
            }
            //直接插入2
            int[] nums2 = new[] { 6, 5, 3, 1, 8, 7, 2, 4 };
            for (int i = 0; i < nums2.Length; i++)
            {
                int temp = nums2[i];//记录需要排序的数字(提取未排序序列中的数字)
                int j;
                for (j = i; j > 0 && nums2[j - 1] > temp; j--)//从后往前遍历,需找到合适的数字进行交换(放入到已排序序列中的数字)
                {
                    nums2[j] = nums2[j - 1];
                    nums2[j-1] = temp;
                }         
            }
        }

这里直接插入2稍微根据直接插入1优化了一下,原理都是一样的。

 

 

算法2:选择排序

第一步:首先在未排序的序列中找到最大(最小)元素,存放到排序序列的其实位置

第二步:再从剩余未排序元素中继续寻找最大(最小)元素,然后放到已排序序列的末尾

第三部:重复第二步,直到所有元素排序完成

过程动画:https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html

   public static void SelectSort()
        {
            int[] nums1 = new[] { 6, 5, 3, 1, 8, 7, 2, 4 };
            int temp;
            for (int i = 0; i < nums1.Length - 1; i++)
            {
                int minVal = nums1[i];
                int minIndex = i;
                for (int j = i + 1; j < nums1.Length; j++)//在未排序的序列中进行选择
                {
                    if (minVal > nums1[j])
                    {
                        minVal = nums1[j];
                        minIndex = j;
                    }
                }
                temp = nums1[i];
                nums1[i] = nums1[minIndex];
                nums1[minIndex] = temp;
            }
        }

 

说道这里可能会想到“直接选择”和“直接插入”俩个排序的关系。这俩个都带有直接,说明这个排序应用实现都比较简单大笑,嘿嘿同时都是将数字分成一个有序区和无序区。不同的就是直接插入:是将无序区域的第一个元素直接插入到有序区域形成一个更大的有序区,直接选择:是从无序区域中选择一个最小的元素然后放到有序区域。

 

说白了就是直接选择首先进行了选取然后在放到有序区域里,插入直接拿出无序的第一个元素然后在有序区域里寻找合适的位置进行排序。

这里转载一篇博文讲的不错是选择排序和插入排序的:https://blog.youkuaiyun.com/dd864140130/article/details/50845945

 

算法3:希尔排序

希尔是个人,他发明了”递减增量排序算法“,即就是一种插入排序的高效性改进。但我估计名字太不好记,索性直接拿人名来命名算法了。希尔排序如最顶上的图片所示是一种非稳定的排序算法。

第一步:选择一个增量序列t1,t2,...,tk其中ti>tj,tk=1(这里是前辈们的经验值);

第二步:按照增量序列个数k,对序列进行k趟排序;

第三部:每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m的子序列,分别对各个子序列进行插入排序。仅增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度。

希尔排序是基于插入排序的以下俩点性质提出改进的:

第一个:插入排序在对已经拍好的序列数据操作时,效率高。

第二个:插入排序每次循环只能移动一个数据

所以希尔排序的整体思想是:先将整个待排序的记录序列分割成若干个子序列分别进行插入排序,等待整个序列中记录基本有序状态时候,在对全体进行直接插入排序。

国外大学的做的希尔排序的过程动画:https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html

 public static void SheelSort()
        {
            int[] nums1 = new[] { 6, 5, 3, 1, 8, 7, 2, 4 };
            int i, j, temp;
            for (int group = nums1.Length / 2; group > 0; group /= 2)//创建增量,nums1长度为8所以会创建出4,2,1的增量因子,并且最小的为1
            {
                for (i = group; i < nums1.Length; i++)//根据创建的增量因子来确定数组上的数字。增量因子为4时就对应着数组的8,num1[4]=8。
                {
                    for (j = i - group; j >= 0; j -= group)//增量因子确定的数字跟数组位置相差同样增量因子的进行比较
                    {
                        if (nums1[j] > nums1[j + group])//判断是否需要数值交换
                        {
                            temp = nums1[j];
                            nums1[j] = nums1[j + group];
                            nums1[j + group] = temp;
                        }
                        else
                        {
                            break;
                        }
                    }
                }
            }
        }

算法写下来也就是:寻找增量因子,然后从增量因子开始寻找所对应数字,然后找到所对的数字再跟数组位置上相差相同的增量因子的位置上的数字做比较。

 

算法4:冒泡排序

正如名字所示泡泡。这个算法在大量数据面前是如此的脆弱,实在有点耗。但却是最基础的算法了。

是一种最简单最直观的的排序算法。它重复走过访问数列一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

第一步:比较相邻的元素。如果第一个比第二个大,就交换他们两个。

第二步:对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

第三步:针对所有的元素重复以上的步骤,除了最后一个。

第四步:持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

过程动画:https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html

 public static void BubbleSort()
        {
            int[] nums1 = new[] { 6, 5, 3, 1, 8, 7, 2, 4 };
            int i, j, temp;
            for (j = 0; j < nums1.Length - 1; j++)
            {
                for (i = 0; i < nums1.Length - 1 - j; i++)
                {
                    if (nums1[i] > nums1[i + 1])
                    {
                        temp = nums1[i];
                        nums1[i] = nums1[i + 1];
                        nums1[i + 1] = temp;
                    }
                }
            }
        }

 

算法5:归并排序

归并排序是建立在归并操作上的一种有效排序算法。该方法采用分治法

分治法:在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……,所以这种方法就可以用

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

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

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

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

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

过程动画:https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html

      public static void MergeCreateSort()
        {
            int[] nums1 = new[] { 6, 5, 3, 1, 8, 7, 2, 4 };
            CreateSort(nums1);
        }
        /// <summary>
        /// 创建分数组
        /// </summary>
        /// <returns></returns>
        private static int[] CreateSort(int[] nums)
        {
            if (nums == null || nums.Length <= 1)
            {
                return nums;
            }
            int avg = nums.Length >> 1;//寻找中间值
            int[] left = new int[avg];//生成左边数组
            int[] right = new int[nums.Length - avg];//生成右边数组
            int[] result = new int[nums.Length];//结果数组

            for (int i = 0; i < nums.Length; i++)//分割数组
            {
                if (i < avg)
                {
                    left[i] = nums[i];
                }
                else
                {
                    right[i - avg] = nums[i];
                }
            }
            left = CreateSort(left);
            right = CreateSort(right);
            result = MergeSort(left, right);//合并数组
            return result;
        }
        /// <summary>
        /// 合并数组
        /// </summary>
        /// <returns>返回合并数组</returns>
        private static int[] MergeSort(int[] left, int[] right)
        {
            int[] result = new int[left.Length + right.Length];
            int i = 0, j = 0, k = 0;
            while (i < left.Length && j < right.Length)
            {
                if (left[i] < right[j])
                {
                    result[k++] = left[i++];
                }
                else
                {
                    result[k++] = right[j++];
                }
            }
            while (i < left.Length)
            {
                result[k++] = left[i++];
            }
            while (j < right.Length)
            {
                result[k++] = right[j++];
            }
            return result;//返回合并数组
        }

归并排序是稳定排序的,但是相对于快速排序是比较慢的。

 

算法6:快速排序

快速排序(Quicksort)是对冒泡排序的一种改进。 快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

一次快速排序的算法是: 

第一步:设置两个变量i、j,排序开始的时候:i=0,j=N-1;

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

第三步:从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;

第四步:从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;

第五步:重复第3、4步,直到i=j; 三,四步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。

过程动画:https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html

 

百度百科示例

假设用户输入了如下数组:

 

下标

0

1

2

3

4

5

数据

6

2

7

3

8

9


创建变量i=0(指向第一个数据), j=5(指向最后一个数据), k=6(赋值为第一个数据的值)。
我们要把所有比k小的数移动到k的左面,所以我们可以开始寻找比6小的数,从j开始,从右往左找,不断递减变量j的值,我们找到第一个下标3的数据比6小,于是把数据3移到下标0的位置,把下标0的数据6移到下标3,完成第一次比较:

 

下标

0

1

2

3

4

5

数据

3

2

7

6

8

9


i=0 j=3 k=6
接着,开始第二次比较,这次要变成找比k大的了,而且要从前往后找了。递加变量i,发现下标2的数据是第一个比k大的,于是用下标2的数据7和j指向的下标3的数据的6做交换,数据状态变成下表:

 

下标

0

1

2

3

4

5

数据

3

2

6

7

8

9


i=2 j=3 k=6
称上面两次比较为一个循环。
接着,再递减变量j,不断重复进行上面的循环比较。
在本例中,我们进行一次循环,就发现i和j“碰头”了:他们都指向了下标2。于是,第一遍比较结束。得到结果如下,凡是k(=6)左边的数都比它小,凡是k右边的数都比它大:

下标

0

1

2

3

4

5

数据

3

2

6

7

8

9


如果i和j没有碰头的话,就递加i找大的,还没有,就再递减j找小的,如此反复,不断循环。注意判断和寻找是同时进行的。然后,对k两边的数据,再分组分别进行上述的过程,直到不能再分组为止。

 

   public static void QuickSort()
        {
            int[] nums1 = new[] { 6, 5, 3, 1, 8, 7, 2, 4 };
            QSort(nums1, 0, nums1.Length - 1);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="nums"></param>
        /// <param name="low">最低位的指针</param>
        /// <param name="high">最高位的指针</param>
        private static void QSort(int[] nums, int low, int high)
        {
            if (low > high)//最低指针必须小于最高位的指针才可以运行下去
            {
                return;
            }
            int first = low;
            int last = high;
            int key = nums[first];
            while (first < last)//主体循环把比key大的放在右边,比他小的放在左边
            {
                //先寻从后往前寻找比key小的数字,找到一个就行
                while (first < last && nums[last] >= key)
                {
                    --last;//高位指针减小
                }
                nums[first] = nums[last];//找到一个比nums[first]小的数字进行对换
                //再从前往后寻找比key大的数字,找到一个就行
                while (first < last && nums[first] <= key)
                {
                    ++first;//低位指针减小
                }
                nums[last] = nums[first];//找到一个比nums[first]大的数字进行对换
            }
            nums[first] = key;
            QSort(nums, low, first - 1);//递归调用
            QSort(nums, first + 1, high);//递归调用
        }

关于快速排序和归并排序:

 

归并排序,简单来说就是先将数组不断细分成最小的单位,然后每个单位分别排序,排序完毕后合并,重复以上过程最后就可以得到排序结果。

快速排序,简单来说就是先选定一个基准元素,然后以该基准元素划分数组,再在被划分的部分重复以上过程,最后可以得到排序结果。

 

算法7:堆排序

首先先要知道一下什么是二叉堆。二叉堆是完全二叉树或者是近似完全二叉树。

二叉堆满足俩个特性:

第一个:父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。

第二个:每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。

当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。下图展示一个最小堆:

大根堆和小根堆堆:

是一颗完全二叉树。

大根堆:所有节点的子节点比其自身小的堆

小根堆:所有节点的子节点比其自身大的堆

堆是一种逻辑结构,物理存储结构中是通过数组这样的结构实现的。

堆与数组的关系,逻辑节点和数组节点关系:i结点的父结点下标为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如第0个结点左右子结点下标分别为1和2。

所以堆的插入逻辑可以简单的写成:

//  新加入i结点  其父结点为(i - 1) / 2  
void HeapAdd(int a[], int i)  
{  
    int j, temp;   
    temp = a[i];  
    j = (i - 1) / 2;      //父结点  
    while (j >= 0 && i != 0)  
    {  
        if (a[j] <= temp)  
            break;           
        a[i] = a[j];     //把较大的子结点往下移动,替换它的子结点  
        i = j;  
        j = (i - 1) / 2;  
    }  
    a[i] = temp;  
}  

堆的删除:

//  从i节点开始调整,n为节点总数 从0开始计算 i节点的子节点为 2*i+1, 2*i+2  
void HeapRemove(int a[], int i, int n)  
{  
    int j, temp;  
    temp = a[i];  
    j = 2 * i + 1;  
    while (j < n)  
    {  
        if (j + 1 < n && a[j + 1] < a[j]) //在左右孩子中找最小的  
            j++;  
        if (a[j] >= temp)  
            break;  
        a[i] = a[j];     //把较小的子结点往上移动,替换它的父结点  
        i = j;  
        j = 2 * i + 1;  
    }  
    a[i] = temp;  
} 

算法步骤:

第一步:将要排序的数组创建为一个大根堆。大根堆的堆顶元素就是这个堆中最大的元素。

第二步:将大根堆的堆顶元素和无序区最后一个元素交换,并将无序区最后一个位置例入有序区,然后将新的无序区调整为大根堆。

第三步:重复操作,无序区在递减,有序区在递增。 初始时,整个数组为无序区,第一次交换后无序区减一,有序区增一。 每一次交换,都是大根堆的堆顶元素插入有序区,所以有序区保持是有序的。

       public static void HeapSort()
        {
            int[] nums1 = new[] { 6, 5, 3, 1, 8, 7, 2, 4 };
            BuildMaxHeap(nums1);
            for (int i = nums1.Length - 1; i > 0; i--)
            {
                Swap(ref nums1[0], ref nums1[i]); //将堆顶元素依次与无序区的最后一位交换(使堆顶元素进入有序区)
                MaxHeapify(nums1, 0, i); //重新将无序区调整为小顶堆
            }
        }
        /// <summary>
        /// 创建大根堆
        /// </summary>
        private static void BuildMaxHeap(int[] nums)
        {
            for (int i = nums.Length / 2 - 1; i >= 0; i--)
            {
                MaxHeapify(nums, i, nums.Length);
            }
        }
        /// <summary>
        /// 大顶堆调整过程
        /// </summary>
        /// <param name="nums">待调整的数组</param>
        /// <param name="i">待调整元素在数组中的位置(即:根节点)</param>
        /// <param name="length">堆中所有元素的个数</param>
        private static void MaxHeapify(int[] nums, int currentIndex, int heapSize)
        {
            int left = 2 * currentIndex + 1;//左子节点在数组中的位置
            int right = 2 * currentIndex + 2;//右子节点在数组中的位置
            int large = currentIndex;//记录此根节点、左子节点、右子节点 三者中最大值的位置

            if (left < heapSize && nums[left] > nums[large]) //与左子节点进行比较
                large = left;
            if (right < heapSize && nums[right] > nums[large]) //与右子节点进行比较
                large = right;
            if (currentIndex != large)//如果 currentIndex != large 则表明 large 发生变化(即:左右子节点中有大于根节点的情况)
            {
                Swap(ref nums[currentIndex], ref nums[large]);
                MaxHeapify(nums, large, heapSize); //以上次调整动作的large位置(为此次调整的根节点位置),进行递归调整
            }
        }
        private static void Swap(ref int a, ref int b)
        {
            int temp = 0;
            temp = a;
            a = b;
            b = temp;
        }

说白了个人认为就是一种间隔排序。

 

算法8:基数排序

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。  说基数排序之前,我们简单介绍桶排序:  

算法思想:是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。 简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。  例如要对大小为[1..1000]范围内的n个整数A[1..n]排序  首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集合B[2]存储   (10..20]的整数,……集合B[i]存储(   (i-1)*10,   i*10]的整数,i   =   1,2,..100。总共有  100个桶。  然后,对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。  再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任  何排序法都可以。  最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这  样就得到所有数字排好序的一个序列了。  假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果  对每个桶中的数字采用快速排序,那么整个算法的复杂度是  O(n   +   m   *   n/m*log(n/m))   =   O(n   +   nlogn   –   nlogm)  从上式看出,当m接近n的时候,桶排序复杂度接近O(n)  当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的  ,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。  前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是:  1)首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。  2)其次待排序的元素都要在一定的范围内等等。

基数排序的方式有两种:MSD或LSD,MSD是从数据的高位(左边)开始,而LSD是从数据的低位(右边)开始。

上面一大串我都没看明白什么意思,但是总的来拿LSD来说就是一个找到每个数字的各位,十位,百位,等等以此类推。然后先比较各位大小排序,得到新的数组;然后是十位大小排序,得到新的数组,以此类推...,最终就可以排出大小顺序。我对基数排序理解也不深,但是有很多地方应用。以后看到再加强理解。可以看一下演示动画基本上一看就明白大体是个啥

基数排序演示动画:https://www.cs.usfca.edu/~galles/visualization/RadixSort.html

 public static void RadixSort()
        {
            int[] num1 = new int[10] { 1001, 223, 932, 194, 14, 220, 650, 390, 8, 123 };
            int[,] temp = new int[10, 10];
            int[] order = new int[10];
            int n = 1;
            int k, lsd;
            while (n <= 1000)//1000:10的3(所给最大数据的位数-1)次方
            {
                k = 0;
                for (int i = 0; i < 10; i++)
                {
                    lsd = (num1[i] / n) % 10;
                    temp[lsd, order[lsd]] = num1[i];
                    order[lsd]++;
                }
                for (int i = 0; i < 10; i++)
                {
                    if (order[i] != 0)
                    {
                        for (int j = 0; j < order[i]; j++) //将temp桶内的数据转入data数组内  
                        {
                            num1[k] = temp[i, j];
                            k++;
                        }
                    }
                    //对每个计数器清零
                    order[i] = 0;
                }
                n *= 10;
            }
        }

感觉写的例子不是很好以后有空再改一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值