C++数据结构和算法:排序算法

为了便于测试,先写一个生成随机数组的方法。

 1 pair<int*,int> GenerateRandomArray(int maxSize, int maxValue, int minValue)
 2 {
 3     //随机数组长度
 4     const int len = rand()%maxSize+1;
 5     int* arr = new int[len];
 6     
 7     for (int i = 0; i < len; i++)
 8     {
 9         arr[i] = rand()%(maxValue-minValue+1)+minValue;
10     }
11     return make_pair(arr,len);
12 }

返回值(数组,数组长度)

 

//////////冒泡排序//////////

第一轮:从第0位开始,依次比较 i 和 i+1 位,把大的换到 i+1 位,一直比较到 i+1 = N

第二轮:从第0位开始,依次比较 i 和 i+1 位,把大的换到 i+1 位,一直比较到 i+1 = N-1

......

第N-1轮:从第0位开始,比较 0 和 1 位,大的换到第1位,比较结束。

 1 void BubbleSort(int arr[], int N)
 2 {
 3     for (int i = N-1; i > 0; i--) //第一轮比较了N-1次,第二轮N-2次,...,一直到1次
 4     {
 5         for (int j = 0; j < i; j++) //注意最大值是j+1=i,所以要比较到j<i
 6         {
 7             if (arr[j] > arr[j + 1])
 8                 swap(arr[j], arr[j + 1]);
 9         }
10     }
11 }

 

//////////选择排序//////////

第一轮:从第0位开始,一直到最后一位,找到最小的数,放在第0位

第二轮:从第1位开始,一直到最后一位,找到最小的数,放在第1位

......

第N-1轮:从第N-2位开始,比较第N-2和第N-1位,也就是最后两位,找到最小的放在N-2位,排序结束。

 1 void SelectionSort(int arr[], int N)
 2 {
 3     for (int i = 0; i < N - 1 ; i++)  //只剩最后一位时不用比较,所以一共比较了N-1次
 4     {
 5         int minIdx = i;
 6         for (int j = i + 1; j < N; j++)  //已经把第i位设为当前最小值,直接从i+1开始比到最后一位
 7         {
 8             if (arr[j] < arr[minIdx])
 9                 minIdx = j;
10         }
11         swap(arr[minIdx], arr[i]);
12     }
13 }

 

//////////插入排序//////////

第一轮:从第1位往前到第0位,逐次比较,把更小的换到左边,共比较1次。

第二轮:从第2位往前到第0位,逐次比较,把更小的换到左边,共比较2次。

......

第N-1轮:从第N-1位往前到第0位,逐次比较,把更小的换到左边,共比较N次。

===>外循环:i:1~N-1;

===>内循环:j:i~0;

最简单的可以类比扑克牌抽牌理牌,每抽起来一张牌都从右往左一一比较,然后插入到合适位置。

 1 void InsertionSort(int arr[], int N)
 2 {
 3     for (int i = 1; i < N; i++)
 4     {
 5         for (int j = i - 1; j >= 0; j--)
 6         {
 7             if (arr[j] > arr[j + 1])
 8                 swap(arr[j], arr[j + 1]);
 9         }
10     }
11 }

 

时间复杂度:冒泡、选择、插入的时间复杂度都为O(N2)

下面介绍两种时间复杂度为O(NlogN)的算法:归并排序、快速排序(最坏情况是O(N2))

在此之前,先介绍递归

//////////递归//////////

Q. 在[L,R]范围内找最大值

 1 int MaxValue(int arr[], int L, int R)
 2 {
 3     //[L,R] 只有1个数时,直接返回这个数
 4     if (L == R)
 5         return arr[L];
 6     int mid = L + ((R - L) >> 1);
 7     int leftMax = MaxValue(arr, L, mid);
 8     int rightMax = MaxValue(arr, mid + 1, R);
 9     return max(leftMax, rightMax);
10 }

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

小技巧:如何选取中间节点?

mid = ( L + R ) / 2 ???    × 当数字特别大时,L+R可能会溢出

mid = L + ( R - L ) / 2     √ 避免溢出

还可以效率更高:mid = L + (R - L) >> 1 

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Tips. 不建议用递归,容易栈溢出

∵ 每当程序执行一个函数调用,就会增加一层栈帧,当函数返回才会减一层栈帧。递归调用达到结束点才会退栈清栈。

∴ 可以用动态规划代替递归 或 修改栈的大小

//////////归并排序//////////

 1 void Merge(int arr[], int low, int mid, int high)
 2 {
 3     int i = low, j = mid + 1;
 4     vector<int> tmpArr;
 5     while (i <= mid && j <= high)
 6     {
 7          if (arr[i] < arr[j])
 8             tmpArr.push_back(arr[i++]);
 9         else
10             tmpArr.push_back(arr[j++]);
11           }
12     while (i <= mid)
13         tmpArr.push_back(arr[i++]);
14     while (j <= high)
15         tmpArr.push_back(arr[j++]);
16     for (int n = 0; n <= high-low; n++)
17         arr[low+n] = tmpArr[n];
18 }
19 
20 //归并排序
21 void MergeSort(int arr[],int L, int R)
22 {
23     if (L >= R)    return;
24     int mid = L + ((R - L) >> 1);
25     MergeSort(arr,L, mid);                 //左边排好序
26     MergeSort(arr,mid + 1, R);             //右边排好序
27     Merge(arr, L, mid, R);                 //整体有序
28 }

 

Q. 荷兰国旗问题:给定一个数组arr,和一个数num,把小于num的数放在数组左边,等于num的数放在中间,大于num的数放右边,要求空间复杂度O(1),时间复杂度O(N)

思路:最左和最右两个指针i,j,指向当前位置的cur,从左往右依次遍历,如果比num大就跟j交换位置,j--;如果比num小就跟i交换位置,i++,cur++;如果等于num不用交换直接cur++。

 1 void QuickSort1(int arr[], int len, int num)
 2 {
 3     int i = 0;  //小于区域指针
 4     int j = len-1;  //大于区域指针
 5     int cur = 0;    //当前指针
 6 
 7     while (cur < j)
 8     {
 9         if (arr[cur] < num)
10         {
11             swap(arr[cur], arr[i]);
12             i++;
13             cur++;
14         }
15         else if (arr[cur] == num)
16             cur++;
17         else
18         {
19             swap(arr[cur], arr[j]);
20             j--;
21         }
22     }
23 }

 

//////////快速排序//////////

 1 void QuickSort(int arr[], int L, int R)
 2 {
 3     int i = L;
 4     int j = R;
 5     int temp = arr[i];   //基准值
 6     if (i >= j)    return;
 7     while (i < j)
 8     {
 9         while (i < j && arr[j] >= temp)    j--;   //从右往左找到比基准值小的值
10         if (i < j)
11             arr[i++] = arr[j];  //将找到的比基准值小的数换到左边
12         while (i < j && arr[i] <= temp)    i++;  //从左往右找到比基准值大的数
13         if (i < j)
14             arr[j--] = arr[i];  //将找到的比基准值大的数换到右边
15         arr[i] = temp;  //最后把基准数放在i位置
16     }
17     QuickSort(arr, L, i - 1);
18     QuickSort(arr, i + 1, R);
19 }

 

///大根堆///

逻辑上是一棵完全二叉树

    0                           i 的左孩子序号是:2i+1

 1     2                            右孩子序号是:2i+2

3 4  5 6                          父节点序号是:(i-1)/2

每一棵子树的最大值是根节点,小根堆同理。

 1 void HeapInsert(int arr[], int index)
 2 {   
 3     while (arr[index] > arr[(index - 1) / 2])  //比父节点大则交换
 4     {
 5         swap(arr[index], arr[(index - 1) / 2]);
 6         index = (index - 1) / 2;    //继续向上检查父节点
 7     }
 8 }
 9 int main()
10 {
11         for (int i = 0; i < len; i++)
12             HeapInsert(arr, i);    
13 }

eg. 原始数组: 26 31 48 8 22

按大根堆插入之后:48 26 31 8 22

      26                        48

  31   48      ==>     26    31

8  22                    8   22

 上面介绍了如何向上检查是否是大根堆,下面介绍如何向下检查:

 1 void Heapify(int arr[], int index, int heapsize)
 2 {
 3     int left = index * 2 + 1;   //左孩子的下标
 4     while (left < heapsize)    //如果左孩子下标没有越界,说明有左孩子
 5     {
 6         //比较左右孩子谁大赋给largest
 7         int largest = left + 1 < heapsize && arr[left + 1] > arr[left] ? left + 1 : left;
 8         //比较大孩子跟父亲谁大赋给largest
 9         largest = arr[largest] < arr[index] ? index : largest;
10         //如果父节点最大,不用交换,退出循环
11         if (largest == index)  break;  
12         //更大孩子与父节点交换位置,继续向下检查是否是大根堆
13         swap(arr[largest], arr[index]);
14         index = largest;
15         left = index * 2 + 1;
16     }
17 }

//////////堆排序//////////

 1 void HeapSort(int arr[], int heapSize)
 2 {
 3     if (arr == nullptr || heapSize < 2)
 4         return;
 5     //向上检查大根堆,结果是最大的数排在根节点
 6     for (int i = 0; i < heapSize; i++)   //O(N)
 7         HeapInsert(arr, i);              //O(logN)
 8 
 9     //把最大的数放到最后,heapSize理解为未排序的堆大小
10     swap(arr[0], arr[--heapSize]);
11 
12     while (heapSize > 0)               //O(N)
13     {
14         //向下检查大根堆,会把未排序最大值排到根节点
15         Heapify(arr, 0, heapSize);    //O(logN)
16         //交换根节点与未排序末尾
17         swap(arr[0], arr[--heapSize]);
18     }
19 }  

时间复杂度:O(NlogN)

过程演示:

         7                                           28            

    27    28   ==HeapInsert==>    7    27   

4                                              4                     

                                              4                                                

==heapSize=3,Swap==>   7    27    

                                       28

                                 27                                                                                                     

==Heapify==>     7       4     

                      28                          

                                                 4      

==heapSize=2,Swap==>     7      27  

                                        28             

                                7      

==Heapify==>    4     27   

                       28  

                                               4   

==heapSize=1,Swap==>   7     27   

                                       28   

                              4              

==Heapify==>  7     27  

                     28      

                                              4   

==heapSize=0,Swap==>  7     27

                                     28

 

//////////基数排序//////////

 原理:arr = {1,35,100,40,19} 

数组中最大元素是3位(100),所以执行轮数是3轮

0~9 十个桶,每次按第i位数去装桶,再按先进先出原则输出顺序。

第一轮,比较 i=1 位

             40

            100  1                     35                    19

bucket:0    1    2    3    4    5    6    7    8    9

输出:100 40 1 35 19

第二轮:比较 i=2 位

              1

            100  19       35   40

bucket:0    1    2    3    4    5    6    7    8    9

输出:100 1 19 35 40

第三轮:比较 i=3 位

             40

             35

             19

              1   100  

bucket:0    1    2    3    4    5    6    7    8    9

输出:1 19 35 40 100 

找到最大位数:

 1 int MaxBits(int arr[], int len)
 2 {
 3     int max = 0;
 4     for (int i = 0; i < len; i++)
 5         max = max > arr[i] ? max : arr[i];
 6     int res = 0;
 7     while (max != 0)
 8     {
 9         res++;
10         max /= 10;
11     }
12     return res;
13 }

 获取数字x的第d位数:

1 int GetDigit(int x, int d)
2 {
3     return ((x / (int)pow(10, d - 1)) % 10);
4 }

长度为1不用排直接返回,否则进行基数排序:

1 void RadixSort(int arr[],int len)
2 {
3     if (arr == nullptr || len < 2)
4         return;
5     RadixSort(arr, 0, len, MaxBits(arr, len));
6 }

基数排序:每轮排序用到了键索引计数法(参考 上一篇字符串相关算法)

 1 void RadixSort(int arr[], int L, int R, int digit)
 2 {
 3     int i = 0, j = 0;
 4     vector<int> bucket;
 5     for (int i = 0; i < R; i++)
 6         bucket.push_back(0);
 7 
 8     //根据最大位数确定轮数
 9     for (int d = 1; d <= digit; d++)
10     {
11         int count[10] = { 0 };  //用于计算出现频率,count[i]代表d位≤i的数有多少个
12         for (int i = L; i <= R; i++)
13         {
14             j = GetDigit(arr[i], d);
15             count[j]++;
16         }
17         for (int i = 1; i < 10; i++)
18             count[i] += count[i - 1];
19 
20         for (i = R-1; i >= L; i--)
21         {
22              j = GetDigit(arr[i], d);
23             bucket[count[j] - 1] = arr[i];
24             count[j]--;
25         }
26         for (int i = L, j = 0; i < R; i++, j++)
27             arr[i] = bucket[j];
28     }
29 }

Tips. 上面这段代码函数返回时报count数组附近栈去损坏,还没找到原因

 

排序稳定性:不因为排序而改变相对次序就是稳定,比如 1 2 1 3,排序后 1 1 2 3,排序后的第一个1在原来的数组里也是第1个1,两个1的相对位置不变。

稳定的排序:冒泡、插入、归并、桶排序

不稳定的排序:选择、快排、堆排序

那么这个属性有什么用呢?比如多条件排序,先用了按时间排序,对于稳定的排序,再按下按价格排序,就会把最近最便宜的排在最上面,但如果用了不稳定的排序,那么添加第二个条件的时候最便宜的在最上面,但不一定是时间最近的了。

 

posted @ 2022-11-30 15:34  番茄玛丽  阅读(53)  评论(0编辑  收藏  举报