排序梳理(升序)
一、大体排序算法的实现
1.插入排序
思想:对于一排数据,依次按顺序处理每个数,在处理数的过程中,将要处理的数当做新插入的一个数,对前面有序的数依次比较,如果处理的数小于其前面的数就让该数向后移动(因为处理的数一定是会放到前面的),直到处理的数大于某个数或者前面没有数进行比较就结束,该处理的数与该数交换。
代码详解
辅助代码更好理解
//先写单趟更好分析: // Eg: // 2 4 8 9 3 0 //1.就如我上述所说,从9开始进行处理 //2.因为我说过会对数据进行移动,所以进行处理的数就要提前进行保留,我们定义一个临时变量tmp //3.因为我们要找到小于处理数进行交换,所以我们也需要一个下标进行标识方便进行交换 //单趟演示: int tmp=a[i];//i是要处理的数的下标 int end=i-1; while(end>=0)//直到走到数据结束 { if(tmp>a[end]) break;//结束 else { a[end+1]=a[end];//向后移动给前面插入数据腾出空间 end--;//在继续寻找 } } //到这里有两种情况 //1.end对应的数小于处理的数,则处理数插入到end的后面一个数 //2.处理的数是其前面数中最小的一个,此时end为负一,处理的数也是插入end的后面的一个数 即: swap(a[end+1],tmp); //总趟数 //无非就是遍历一遍数据套个for循环 for(int i=1;i<n;i++)//第一个数可以不用处理
实现结果和总的代码:
void InsertSort(int* a,int n) { //再写整体 for (int i = 1; i < n; i++) { int tmp = a[i]; int end = i - 1; while (end >= 0) { if (tmp > a[end]) break; else { a[end + 1] = a[end]; end--; } } swap(tmp, a[end + 1]); } }
2.希尔排序
思想:本质的思想和插入排序没什么不同,就是将数组分为几组进行插入排序,将数组先趋近于一个有序的状态。这样可以更好的进行插入排序。
代码详解
//一、先写单趟好分析 // Eg: // 17 9 1 22 7 15 5 19 16 18 //1.分组以gap为标记,相邻为gap的数为一组进行插入排序(后续再对gap的取值进行说明) //2.进行插入排序 //先写单趟(思路和插入排序一样,不同的就是两个数的距离是gap) for (int i = gap; i < n; i += gap)//每个数之间的距离为gap { int tmp = a[i]; int end = i - gap; while (end >= i - gap) { if (tmp > a[end]) break; else { a[end + gap] = a[end]; end -= gap; } } swap(tmp, a[end + gap]); } //二、再写多趟,无非就是在对gap个数据进行分别的插入排序 for(int j=0;j<gap;j++) //其中要注意的就是for(int i=gap+j;i<n;i+=gap)这一处变化 //三、再套一层gap即可 //gap的取法有两种,就是一直除2,或者除以3在加一(为了避免没有没有1可以进行插入排序) while(gap) { code gap/=2; }
实现结果和总的代码:
void ShellSort(int* a, int n) { //分组 int gap = n / 2 ; while (gap) { for (int j = 0; j < gap; j++) { //先写单趟 for (int i = gap+j; i < n; i += gap) { int tmp = a[i]; int end = i - gap; while (end >= i - gap) { if (tmp > a[end]) break; else { a[end + gap] = a[end]; end -= gap; } } swap(tmp, a[end + gap]); } } gap /= 2; } } //还可以再优化一下 void ShellSort(int* a, int n) { //分组 int gap = n / 2 ; while (gap) { //先写单趟 for (int i = gap; i < n; i++) { int tmp = a[i]; int end = i - gap; while (end >= i - gap) { if (tmp > a[end]) break; else { a[end + gap] = a[end]; end -= gap; } } swap(tmp, a[end + gap]); } gap /= 2; } }
3.选择排序
思想:很简单,就是现在一个区间内选出最大的数和最小的数然后再缩小区间范围。
代码详解
//1.首先定义闭区间左右下标left和right和标记极大值max和极小值min //2.在对区间进行遍历查找 int left = 0; int right = n-1; int max = 0; int min = 0; while (left < right) { for (int i = left; i <= right; i++) { if (a[i] > a[max]) max = i; if (a[i] < a[min]) min = i; } //3.再进行最右值和最左值同时与最大值和最小值的交换 //注:交换的时候要注意一种情况 // Eg: // 17 9 8 22 7 //在对该对数据进行实现时,max是3,min是4 //如果先对right与max交换的话,会出现问题就是此时min对应的值会变成22,而不是7,所以我们要进行判断更新一下 swap(a[right],a[max]); if(min==right) min=max; swap(a[min],a[left]);
实现结果和总的代码:
void SelectSort(int*a,int n) { int left = 0; int right = n-1; int max = 0; int min = 0; while (left < right) { min = max = left; for (int i = left; i <= right; i++) { if (a[i] > a[max]) max = i; if (a[i] < a[min]) min = i; } swap(a[right], a[max]); if (min == right) min = max; swap(a[left], a[min]); left++; right--; } }
4.堆排序
思路:简单来说就是建堆之后再将堆顶的数与最底层的数进行交换在进行向下调整。注意的是排升序的话要建大堆。假设排降序首先使用堆排序主要是用堆顶元素,如果使用大根堆排降序,此时堆顶的元素是最大的,当我们取出堆顶元素时,此时大根堆的性质就变了,那么下次就难以找到第二大元素,要重新建堆。
代码详解
//建堆有两种建堆方式 //1.向上调整建堆 //1.向上建堆 //核心思想就是对数据依次处理,将i前面的树实现成大堆 void AdjustUp(int* a, int n) { for (int i = 0; i < n; i++) { int child = i; int parent = (child - 1) / 2; while (child)//这里不能将条件写为parent>=0因为,正数除法不可能出现负数 { if (a[child] > a[parent]) swap(a[child], a[parent]); child = parent; parent = (parent - 1) / 2; } } } //2.向下建堆 //没什么好说的,不同的就是建堆时从最后一个叶节点的父亲开始建小堆 void AdjustDown(int* a, int n) { for (int i = (n - 1) / 2; i >= 0; i--) { int parent = i; int child = 2 * parent + 1; if (child + 1 < n && a[child] < a[child + 1])//判断一下越界问题 child++; while (child<n) { if (a[child] > a[parent]) swap(a[child], a[parent]); parent = child; child = 2 * child + 1; if (child + 1 < n && a[child] < a[child + 1]) child++; } } } //3。由于堆顶的数据一定是最大的,所以将堆顶数据与堆尾数据进行交换,然后在向下调整,除了交换后的数据,依次交换 //交换向下调整 for (int i = n-1; i >=0; i--) { swap(a[0], a[i]); AdjustDown(a, i); }
建议使用向下调整建堆,具体原因在我对所有排序的时间复杂度分析时会给出
实现结果和总的代码:
//1.向上建堆 void AdjustUp(int* a, int n) { for (int i = 0; i < n; i++) { int child = i; int parent = (child - 1) / 2; while (child) { if (a[child] > a[parent]) swap(a[child], a[parent]); child = parent; parent = (parent - 1) / 2; } } } //2.向下建堆 void AdjustDown(int* a, int n) { for (int i = (n - 1) / 2; i >= 0; i--) { int parent = i; int child = 2 * parent + 1; if (child + 1 < n && a[child] < a[child + 1]) child++; while (child<n) { if (a[child] > a[parent]) swap(a[child], a[parent]); parent = child; child = 2 * child + 1; if (child + 1 < n && a[child] < a[child + 1]) child++; } } } void HeapSort(int* a, int n) { //建堆 AdjustDown(a, n); //交换向下调整 for (int i = n-1; i >=0; i--) { swap(a[0], a[i]); AdjustDown(a, i); } }
5.冒泡排序
思想:在第一次的时候把最大的数据冒到数据的,第二次的时候把最大的数据(除了最后一个数据)冒到倒数第二个的位置,依次进行就可以了。
代码详解
//先写单趟 for (int i = 0; i < n - 1; i++) { if (a[i] > a[i + 1]) swap(a[i], a[i + 1]); } //再写整体 //主要控制右边的数据 for(int i=0;i<n-1;i++)//处理到第一个数据的话就没有必要了
实现结果和总的代码:
//再写整体 for (int j = 0; j < n-1; j++) { //先写单趟 for (int i = 0; i < n - 1-j; i++) { if (a[i] > a[i + 1]) swap(a[i], a[i + 1]); } }
6.快速排序(递归)
快速排序总共分为三种实现思路(后面再写非递归)
6.1 Hoare版本
思路: 界定一个值keyi,一般是最左边,进行操作将该值放到其本应该在的位置,具体实现为定义一个左指针,一个右指针,右指针先走(keyi在左边就让右指针先走),如果数据小于keyi停下,左指针开始走,直到数据大于keyi停下,两者进行交换,直到left指针和right指针相遇的位置就是keyi的位置,在进行交换,然后进行递归,[left,keyi-1]和[keyi+1,right]
代码详解
在keyi的取值方式上,如果一个数组是有序的话那么我们进行递归的时候区间每次只会减少一个数,这样的效率就会很低,所以我们可以采用三数取中的方法。
//三数取中 int GetMidNum(int* a, int n) { int begin = a[0]; int mid = a[n / 2]; int end = a[n - 1]; if (begin < end) { if (begin > mid) return begin; else if (mid > end) return end; else return mid; } else { if (begin < mid) return begin; else if (mid < end) return end; else return mid; } }
//先写单次 int begin = left; int end = right; int mid=GetMidNum(a,right-left+1);//通过三数取中保证效率 swap(a[left],a[mid]);//将left与中数进行交换 int keyi =left;//标记值 while (left < right)//当两个指针相遇停止 { while (left < right && a[right] >= a[keyi])//防止越界 right--; while (left < right && a[left] <= a[keyi])//防止越界 left++; swap(a[left], a[right]);//保持标志值的左边均小于标志值,右边大于标志值 } swap(a[keyi], a[right]);//将标志值放到它应该放置的位置 keyi = right; //再进行递归 //当区间只有只有一个数据或者没有数据的时候就截止 if(right-left<1)//当right-left==0是表明还有一个数据,而没有数据则是区间不存在 return; [left,keyi-1]keyi[keyi+1,right] //对这两者区间进行递归
实现结果和总的代码
void QuickSort1(int* a, int left, int right) { int begin = left; int end = right; if (right - left < 1) return; int mid = GetMidNum(a, left,right); swap(a[left], a[mid]); int keyi =left; while (left < right) { while (left < right && a[right] >= a[keyi]) right--; while (left < right && a[left] <= a[keyi]) left++; swap(a[left], a[right]); } swap(a[keyi], a[right]); keyi = right; QuickSort1(a, begin, keyi - 1); QuickSort1(a, keyi+1, end); }
6.2挖坑法
思路:与上面的写法大同小异,不同的是刚开始把最左边的下标为一个坑同时保存keyi对应的值,同样是右指针先走,如果遇到小于a[keyi]的就将数据填入坑中而遇到的新下标为新坑,然后左指针再走,遇到大于a[keyi]的数据填入坑中,依次进行直到两指针相遇,再将保存的值填入相遇的下标即可,再同样进行递归。
代码详解
//先写单次 int mid = GetMidNum(a, left, right);//三数取中 swap(a[left], a[mid]); int key = a[left];//保存最左侧数据 int hole = left;//设置坑位 while (left < right) { while (left < right && a[right] >= key) right--; a[hole] = a[right];//填坑 hole = right;//更新坑位 while (left < right && a[left] <= key) left++; a[hole] = a[left]; hole = left; } a[hole] = key;//最后的数据填充到坑位中
//再写递归 //相同的判断条件 if (right - left < 1) return; //相同的区间处理 [left,hole-1]hole[hole+1,right] QuickSort2(a, begin, hole - 1); QuickSort2(a, hole+1, end);
实现结果和总的代码
void QuickSort2(int* a, int left, int right) { int begin = left; int end = right; if (right - left < 1) return; int mid = GetMidNum(a, left, right); swap(a[left], a[mid]); int key = a[left]; int hole = left; while (left < right) { while (left < right && a[right] >= key) right--; a[hole] = a[right]; hole = right; while (left < right && a[left] <= key) left++; a[hole] = a[left]; hole = left; } a[hole] = key; QuickSort2(a, begin, hole - 1); QuickSort2(a, hole+1, end); }
6.3前后指针法
思路:虽然和前面的方法大致思路相同,都是先对大区间进行一个位置的确认,再对区间细化递归,但就是前面的第一步和之前的方法不同,上面两种方法都是左右指针,而这个是前后指针,定义两个指针,一个指针指针cur用来扫描数据,一个prev用来划分区间,prev的右边大于等于a[keyi],左边(包括prev本身)小于等于a[keyi]。具体实现看下列代码。
代码详解
//先写一次的 int mid = GetMidNum(a, left, right);//三数取中 swap(a[left], a[mid]); int keyi = left;//记录值 int cur = left; int prev = left; while (cur <= right) { while (cur <= right && a[cur] >= a[keyi])//用cur去扫描数组,如果遇到小于的就停止 cur++; if(cur<=right)//当cur没有越界 swap(a[++prev], a[cur]);//因为prev本身是满足小于或等于a[keyi]的条件,于是将prev向后移,将小于等于a[keyi]的值赋给a[++prev]来保证区间的区分正确 cur++;//cur指针继续扫描 } swap(a[prev], a[keyi]);//prev是最后一个满足条件的下标,所以就是保存的值应该在的位置 keyi = prev;
//再写递归 //结束条件一样 if (right-left<1) return ; //对区间递归 [left, keyi-1]keyi[keyi+1,right] QuickSort3(a, left, keyi - 1); QuickSort3(a, keyi + 1, right);
总的代码和实现结果
void QuickSort3(int* a, int left, int right) { if (right - left < 1) return; int mid = GetMidNum(a, left, right); swap(a[left], a[mid]); int keyi = left; int cur = left; int prev = left; //while (cur <= right) //{ // while (cur <= right && a[cur] >= a[keyi]) // cur++; // if(cur<=right) // swap(a[++prev], a[cur]); // cur++; //} while (cur <= right) { if (a[cur] < a[keyi] && a[++prev] != a[cur]) swap(a[cur], a[prev]); cur++; } swap(a[prev], a[keyi]); keyi = prev; QuickSort3(a, left, keyi - 1); QuickSort3(a, keyi + 1, right); }
7.归并排序(递归)
思路:其实相当于无限二分将数据进行归并,比如9,8,7,6这四个数据,要归并的话先分为两个区间9,8和7,6然后9,8再分分别为9和8两个区间,,分到只有个数据就进行归并成8,9同样7,6归并成6,7,(8 9)与(6 7)再归并成(6 7 8 9)。
代码详解
//照样先写一次的情况 //假设mid左边(不包括mid)都是有序的,mid右边(包括mid) int* tmp = (int*)malloc(sizeof(int) * (right - left + 1));//两个区间进行归并后放在临时数组中后进行拷贝 int begin1 = left;int end1 = mid - 1;//标记第一个区间 int begin2 = mid; int end2 = right;//标记第二个区间 int i = 0;//计入拷贝数组的下标 while (begin1 <= end1 && begin2 <= end2)//当有一个数组遍历完就结束 { if (a[begin1] < a[begin2])//谁小谁就先进数组 tmp[i++] = a[begin1++]; else tmp[i++] = a[begin2++]; } while (begin1 <= end1)//对于没遍历完的数据继续进入拷贝数组 tmp[i++] = a[begin1++]; while (begin2 <= end2) tmp[i++] = a[begin2++]; memcpy(a+left, tmp, (right - left + 1)*4);//将归并完的数组拷贝回数组中 //进行递归写法 //同样的停止条件,当数据只有一个的时候停止 if (right - left < 1) return; //此时左右两边的数组均是有序 int mid = (left + right + 1) / 2; MergeSort(a, left, mid - 1); MergeSort(a, mid, right);
总的代码和实现结果
void MergeSort(int* a, int left, int right) { if (right - left < 1) return; //此时左右两边的数组均是有序 int mid = (left + right + 1) / 2; MergeSort(a, left, mid - 1); MergeSort(a, mid, right); int* tmp = (int*)malloc(sizeof(int) * (right - left + 1)); int begin1 = left;int end1 = mid - 1; int begin2 = mid; int end2 = right; int i = 0; while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] < a[begin2]) tmp[i++] = a[begin1++]; else tmp[i++] = a[begin2++]; } while (begin1 <= end1) tmp[i++] = a[begin1++]; while (begin2 <= end2) tmp[i++] = a[begin2++]; memcpy(a+left, tmp, (right - left + 1)*4); free(tmp); }
二、快排与归并排序的非递归写法
1.快速排序
思路:总的来说,递归和非递归不同的点就在处理完一个区间后如何去处理下一个区间,根据我们的经验,我们可以将要处理的区间放在栈中。
代码详解
//首先我们要将我们处理的区间压入栈中 //我们可以将定义pair类型来存储左右区间,也可以一次性存入两个int值都可以,我这里选择第二种 stack<int> s;//用栈去存储区间 s.push(right); s.push(left); while (!s.empty())//如果栈中为空的话就说明所有区间处理好了 { //这里定义两个数据,一个用来保留最初的区间左值,另外一个用来遍历,定义完就可以将数据出栈了 int first = s.top(); int begin = s.top(); s.pop(); //同上 int last = s.top(); int end = s.top(); s.pop(); int mid = GetMidNum(a, begin, end);//同样的三数取中保证效率 swap(a[begin], a[mid]); //我这里采用的还是Hoare的版本对值进行定位 int keyi = begin; while (begin < end) { while (begin < end && a[end] >= a[keyi]) end--; while (begin < end && a[begin] <= a[keyi]) begin++; swap(a[begin], a[end]); } swap(a[keyi], a[end]); keyi = end; //最后写一下入栈的条件,就和之前递归结束的条件一样 [first,keyi-1]keyi[keyi+1,last] if (last - 1 - keyi >= 1) { s.push(last); s.push(keyi + 1); } if (keyi - 1 - first >= 1) { s.push(keyi-1); s.push(first); } }
总的代码和实现结果
void QuickSortNonR(int* a, int left,int right) { stack<int> s;//用栈去存储区间 s.push(right); s.push(left); while (!s.empty()) { int first = s.top(); int begin = s.top(); s.pop(); int last = s.top(); int end = s.top(); s.pop(); int mid = GetMidNum(a, begin, end); swap(a[begin], a[mid]); int keyi = begin; while (begin < end) { while (begin < end && a[end] >= a[keyi]) end--; while (begin < end && a[begin] <= a[keyi]) begin++; swap(a[begin], a[end]); } swap(a[keyi], a[end]); keyi = end; if (last - 1 - keyi >= 1) { s.push(last); s.push(keyi + 1); } if (keyi - 1 - first >= 1) { s.push(keyi-1); s.push(first); } } }
2.归并排序
思路:简单来说就是模拟之前递归的思路,递归的时候是也是不断二分进行操作的,同样我们模拟的时候就也同样从刚开始一个和一个归并,再到后面用半个数组的区间和半个数组的空间进行递归。
代码详解
//先写最简易版的 //模拟第一遍,让gap为1,一个数据和一个数据进行归并 for (int i = 0; i < n; i +=2*gap)//因为归并是两倍gap的数据进行的,所以进行完一组归并后,i要越过gap { int begin1 = i; int end1 = i + gap - 1;//自己分析一下就可以得出 int begin2 = i + gap; int end2 = begin2 + gap - 1; //下面的就是之前归并的方式 int j = 0; while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] < a[begin2]) tmp[j++] = a[begin1++]; else tmp[j++] = a[begin2++]; } while (begin1 <= end1) tmp[j++] = a[begin1++]; while (begin2 <= end2) tmp[j++] = a[begin2++]; memcpy(a + i, tmp, (8*gap)); } delete[] tmp;
基本的逻辑行得通,但是我们还有些情况没有考虑到。
如上图,如果我们多加一个数的话就会出问题,上面告诉我们是 :运行时检查失败#2-在变量‘a’周围的堆栈已损坏,就是说我们在归并的时候,可能越界了,我们可以把每次归并的区间打印出来。
我们知道我们测试的数据只有9位,而这里递归的时候则好几个区间都去了不存在的区间访问,所以我们可以对此处理。
我们可以看到总共有三种情况:
1.end1越界,因为我们是归并完一组就拷贝,所以当end1越界是,end1前的数据到begin1为止都是有序的,所以我们可以暂停操作直接跳出循环。
2.是begin 2越界,同样是begin 2前面的数据,即[begin1,end1]有序,所以我们也没有必要进行下面操作,直接跳出循环即可。
3.是end2越界,但[begin2,某个位置]这个区间不一定有序,所以我们可以将end2设置为n-1,再进行归并。
memcpy(a + i, tmp, (8*gap))//此时这个8就不对了,就相当于sizeof(int)*2*gap //而在对于第三种情况的处理后就不是2*gap的数据,而是end2-i+1的数据。
因此修改后的代码应该是这样:
总的代码和实现结果
void MergeSortNoR(int* a, int n) { //还是说分为两个区间进行划分进行归并 int gap = 1; while(gap<n) { int* tmp = new int[2 * gap]; for (int i = 0; i < n; i +=2*gap) { int begin1 = i; int end1 = i + gap - 1; int begin2 = i + gap; int end2 = begin2 + gap - 1; //printf("[%d,%d]", begin1, end1); //printf("[%d,%d] ", begin2, end2); if (end1 >= n || begin2 >= n) { break; delete[] tmp; } if (end2 >= n) { end2 = n - 1; } printf("[%d,%d]", begin1, end1); printf("[%d,%d] ", begin2, end2); int j = 0; while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] < a[begin2]) tmp[j++] = a[begin1++]; else tmp[j++] = a[begin2++]; } while (begin1 <= end1) tmp[j++] = a[begin1++]; while (begin2 <= end2) tmp[j++] = a[begin2++]; //memcpy(a + i, tmp, (8*gap)); memcpy(a + i, tmp, sizeof(int)*(end2-i+1)); } delete[] tmp; gap *= 2; printf("\n"); } }
三、各种排序算法的时间复杂度和稳定性
1.插入排序
稳定性:在分析稳定性之前,我们要知道什么是稳定性,稳定性指的是当一组数据中出现两个相同的数据,经过排序后,两个数据保留着原来的前后顺序不变,我们就称这个排序是稳定的。
1.1复杂度
最坏情况:当数据是降序时效率最低为O(N^2)
0 1 2 3 4 5 6 7 8 9
进行第i个数的循环时都要进行i-1次比较交换,总共进行i次,则时间复杂度就为O(N^2).
最好情况:当数据是升序时效率最高位O(N)
进行第i个数的循环时只要进行一次比较就可以,则时间复杂度为O(N).
时间复杂度:O(N^2)
空间复杂度:O(1)
1,2稳定性
可以控制为稳定的,即当遇到相同的数据是保持不变,不进行交换即可。
2.希尔排序
2.1复杂度
当gap大于1是希尔排序是对直接插入排序的预排序,当gap为1时,虽然进行的是直接插入排序,但已经是很有顺序的排序了。
所以希尔排序的时间复杂度不好计算,一般就在这个区间内。
时间复杂度:O(N^1.3)
空间复杂度:O(1)
2.2稳定性
不稳定
因为很可能两个相同的数据分到不同的组中,从而使得两个相同数据的位置发生变化。
3.选择排序
3.1复杂度
最坏情况:O(N^2)
最好情况:O(N^2)
因为就算是有序的,还是要遍历整个数组。
时间复杂度:O(N^2)
空间复杂度:O(1)
3.2稳定性
不稳定
考虑以下数组:
[5,8,5,3,2][5a*,8,5b*,3,2]
其中,5�5a 和 5�5b 表示两个相等的元素。现在让我们使用选择排序来对这个数组进行排序。
第一步: 选择最小的元素,将其与第一个元素交换。在这个例子中,最小的元素是 2,与第一个元素 5a 交换。
[2,8,5�,3,5�][2,8,5b,3,5a]
第二步: 在剩余的未排序部分中选择最小的元素,将其与第二个元素交换。在这个例子中,最小的元素是 3,与第二个元素 8 交换。
[2,3,5�,8,5�][2,3,5b,8,5a]
第三步: 在剩余的未排序部分中选择最小的元素,将其与第三个元素交换。在这个例子中,最小的元素是 5a,与第三个元素 5b 交换。
[2,3,5�,8,5�][2,3,5a,8,5b]
现在,观察最终排序结果,可以看到相等的元素 5a 和 5b 的相对顺序发生了变化,原来在 5b 之前的 5a 现在在 5b 之后。这就是选择排序在相等元素之间可能破坏相对顺序的一个例子,证明了选择排序的不稳定性。
4.堆排序
4.1复杂度
分为两个过程:1.建堆 2交换向下调整
1.1向上建堆:以完全二叉树为例:
设树的高度为n,则总共有2^(n)-1个节点第一层有2(0)个节点,分别进行0次调整,总共进行**0*2(0)**次调整
第二层有2(1)个节点,分别进行1次调整,总共进行**1*2(1)**次调整
第三层有2(2)个节点,分别进行2次调整,总共进行**2*2(2)**次调整
………………
第n层有2(n-1)个节点,分别进行n-1次调整,总共进行**(n-1)*2(n-1)**次调整
总共为 0乘2^(0)+ 1乘2(1)+………………+(n-1)乘2(n-1)
根据错位相减可以得出:
我们将总数据为N,代换算为O(N*logN)
1.2向下建堆:以完全二叉树为例:
设树的高度为n,则总共有2^(n)-1个节点
第一层有2(0)个节点,分别进行n-1次调整,总共进行**(n-1)*2(0)**次调整
第二层有2(1)个节点,分别进行n-2次调整,总共进行***(n-2)(1)**次调整
第三层有2(2)个节点,分别进行n-3次调整,总共进行**(n-3)*2(2)**次调整
………………
第n-1层有2(n-2)个节点,分别进行1次调整,总共进行**1*2(n-2)**次调整
第n层有2(n-1)个节点,分别进行0次调整,总共进行**0*2(n-1)**次调整
根据错位相减可以得出:
我们将总数据为N,代换算为O(N)
所以我们一般选择向下建堆
2.交换向下调整
重复从堆顶取出最大元素(对于升序排序)并调整堆,直到堆为空。每次调整堆的操作的时间复杂度为 O(logN)。总的排序过程的时间复杂度为 O(NlogN)。
**时间复杂度:**O(logNN)
空间复杂度:O(1)
4.2稳定性
不稳定
5.冒泡排序
5.1复杂度
最坏情况:O(N^2)
最好情况:O(N)
优化一下
//再写整体 for (int j = 0; j < n-1; j++) { //先写单趟 int flag=0; for (int i = 0; i < n - 1-j; i++) { if (a[i] > a[i + 1]) { swap(a[i], a[i + 1]); flag=1; } } if(!flag) break; }
时间复杂度:O(N^2)
空间复杂度:O(1)
5.2稳定性
稳定
6.快速排序
6.1复杂度
最坏情况:已排序好,右边指针一直走到末尾,不断进行
N*(N-1) *(N-2)…… *1=N^2
但经过三数取中不会出现这种情况
大概都是一分为而二,进行logN次递归,每次进行N次遍历
时间复杂度:O(logN)*
空间复杂度:O(1)
6.2稳定性
不稳定
7.归并排序
7.1复杂度
归并排序采用分治策略,将数组逐步分成较小的子数组,然后递归地对这些子数组进行排序,最后将排好序的子数组合并成一个整体有序的数组。在每一次合并操作中,都会线性地比较和移动元素,所以合并的时间复杂度是 O(n)。因此,整个归并排序的时间复杂度可以通过递归树的高度和每层的合并操作复杂度来计算。
递归树的高度是 O(logn),每一层的合并操作的时间复杂度是 O(n)
时间复杂度: O(NlogN)。
空间复杂度:O(N)
7.2稳定性
稳定
四、补充知识
1.快速排序的优化——三路划分
当我们在掌握上述快速排序到LeetCode去实践时,我们会遇到如下问题:
这是因为当测试用例出现大量相同数据时,每次右指针都要遍历整个数组,造成效率低下
解决方法:
因为之前对与key相同的数据没做处理,无论其在keyi的左边还是右边都可以。
所以我们这里进行处理,规定[begin,left-1] [left,right] [right+1,end] 分别小于key’,等于key,大于key
走双指针的思路。
if (left < right && a[cur] < key)//如果数据小于key,交换,left++ { swap(a[cur], a[left++]); } else if (left<right && a[cur] > key)//如果数据大于key,交换,left-- { swap(a[cur], a[right--]); } else//如果数据相等cur++ cur++; }
void QuickkSort(int* a, int left, int right) { if (right - left < 1) return; int mid = GetMidNum(a, left, right); swap(a[left], a[mid]); int key = a[left]; int begin = left; int end = right; int cur = left; while (cur <= right) { if (left < right && a[cur] < key) { swap(a[cur], a[left++]); } else if (left<right && a[cur] > key) { swap(a[cur], a[right--]); } else cur++; } QuickkSort(a, begin, left - 1); QuickkSort(a, right+1,end); }
1955533.png&pos_id=img-UNAzr4TL-1702907764368)
但当我们继续去测试时,我们会发现还是过不了,但是卡的测试用例不一样了。
ge-20231217222134150.png&pos_id=img-Wlkb1Wb3-1702907764368)
如下,leetcode排序测试用例非常严格,估计设计好几个搞事情的用例。除了验证正确性还考验特殊场景的性能。
1、全是相同值,我们通过三路划分解决了
2、特殊测试用例,如果我们严格走三数取中,可能大量区间选key让你选到比较小或比较大的,导致性能下降。–>解决方案:结合随机选key优化
采用随机选key
针的思路。
if (left < right && a[cur] < key)//如果数据小于key,交换,left++ { swap(a[cur], a[left++]); } else if (left<right && a[cur] > key)//如果数据大于key,交换,left-- { swap(a[cur], a[right--]); } else//如果数据相等cur++ cur++; }
void QuickkSort(int* a, int left, int right) { if (right - left < 1) return; int mid = GetMidNum(a, left, right); swap(a[left], a[mid]); int key = a[left]; int begin = left; int end = right; int cur = left; while (cur <= right) { if (left < right && a[cur] < key) { swap(a[cur], a[left++]); } else if (left<right && a[cur] > key) { swap(a[cur], a[right--]); } else cur++; } QuickkSort(a, begin, left - 1); QuickkSort(a, right+1,end); }
[外链图片转存中…(img-UNAzr4TL-1702907764368)]
但当我们继续去测试时,我们会发现还是过不了,但是卡的测试用例不一样了。
[外链图片转存中…(img-Wlkb1Wb3-1702907764368)]
如下,leetcode排序测试用例非常严格,估计设计好几个搞事情的用例。除了验证正确性还考验特殊场景的性能。
1、全是相同值,我们通过三路划分解决了
2、特殊测试用例,如果我们严格走三数取中,可能大量区间选key让你选到比较小或比较大的,导致性能下降。–>解决方案:结合随机选key优化
采用随机选key