排序算法时间复杂度、空间复杂度、稳定性比较

1、冒泡排序
冒泡排序是通过交换相邻的两个数字使小的在前在的在后,小的数字慢慢“冒”到前面;由于每次循环都要从前到后遍历一次数组,要遍历n轮,因此时间复杂度为O(n^2);由于会在前大后小的情况下发生交换,如果之前满足排序要求则不会发生交换,因此该排序算法稳定。该算法直接在原数组进行操作,只有在交换时需要常数个临时变量,因此空间复杂度为O(n^2)
void bubbleSort(vector<int> &nums) {
int len = nums.size();
for (int i = 0; i < len; ++i) {
for (int j = len-1; j > i; --j) {
if (nums[j] < nums[j - 1])
swap(nums[j], nums[j - 1]);
}
}
}
冒泡算法改进:上述的冒泡算法存在一个问题那就是数组本来就是满足排序需求的但是还是会遍历n轮所有的变量。对此,可以利用一个标志位进行优化,每轮遍历开始的时候flag为0,如果第i轮没有进行交换,则说明数组已经有序不需要在遍历
//利用flag标志位优化冒泡,优化之后最快O(n),一开始就排好序的情况
void bubbleSortImprove(vector<int> &nums) {
int len = nums.size();
int flag = 1;
for (int i = 0; i < len && flag; ++i) {
flag = 0;
//如果没有被更新为1则说明没有再交换,则说明数组已经为有序
for (int j = len - 1; j > i; --j) {
if (nums[j] < nums[j - 1]) {
swap(nums[j - 1], nums[j]);
flag = 1;
}
}
}
2、选择排序
从前到后依次给每个位置选择当前所有元素中最小的数字,假设有n个元素该算法每次遍历选出一个从当前位置到最后一个位置的元素中最小的一个,执行n轮,时间复杂度O(n^2),空间复杂度O(1);在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了,因此该排序算法不稳定
void selectSort(vector<int> &nums) {
int len = nums.size();
for (int i = 0; i < len; ++i) {
int min = i;
for (int j = i + 1; j < len; ++j) {
if (nums[j] < nums[min])
min = j;
}
if (min != i) {//如果当前位置不是最小则交换
swap(nums[min], nums[i]);
}
}
}
3、插入排序
插入排序将数字分为基本有序和无序两部分,每次将无序的数字在基本有序的区间中选择合适的位置插入。要对每个数字选择合适的位置插入,每次插入最多会移动n次,因此时间复杂度为O(n^2),最好的情况是数组按排序要求排列的,这时只用遍历一遍时间复杂度为O(n),空间复杂度为O(1),由于是从前到后依次选择数字和插入位置,因此该排序算法是稳定的。该算法适用于基本有序的数组
void insertSort(vector<int> &nums) {
int len = nums.size();
for (int i = 1; i < len; ++i) {
int temp = nums[i];//看是否需要往前插入
int j = i - 1;//目前要插入的位置
while (j >= 0 && nums[j] > temp) {
nums[j + 1] = nums[j];//如果temp小于前面的数(要插入位置)则将前面的数后移
--j;
}
nums[j + 1] = temp;
}
}
4、希尔排序
希尔排序是将数组分块之后,对每块执行插入排序;其时间复杂度平均情况为O(n^1.3),最好为O(n),最坏为O(n^2);
由于是分块后进行排列,因此其稳定性会被破坏
void shellSort(vector<int> &nums) {
int len = nums.size();
for (int increment = len / 2; increment > 0; increment /= 2) {
for (int i = increment; i < len; ++i) {
int temp = nums[i];
int j = i - increment;
while (j >= 0 && nums[j] > temp) {
nums[j + increment] = nums[j];
j -= increment;
}
nums[j + increment] = temp;
}
}
}
5、快速排序
快排是以一个数字为中心,将数组分为两个部分,升序排列就是令比中心数字小的在前面,比中心数字大的在后面;然后返回排序后中心数字的下标,然后在对前后两部分执行之前的操作,知道排序完成;该算法每次将数组分为两个部分处理,即n+n/2+n/4+.....,因此其时间复杂度为O(nlogn),该算法时候大规模数据,空间复杂度为O(logn)~O(n)
1、先利用partition函数返回中间值的位置
int partition(vector<int> &nums,int left, int right) {
//int left = 0, right = nums.size() - 1;
int key = nums[left];
while (left < right) {
while (left < right && key <= nums[right])--right;
swap(nums[left], nums[right]);
while (left < right && nums[left] <= key) ++left;
swap(nums[left], nums[right]);
}
return left;
}
//2 多次调用是整个数组有序
void quickSort(vector<int> &nums,int left,int right){
if (left >= right)
return;
//int pos = partition(nums, left, right);
int pos = partitionModified(nums, left, right);
quickSort(nums, left, pos-1);
//因为pos位左边的数都比他小右边的都比他大因此,不用再进行排序
quickSort(nums, pos + 1, right);
}
一般选取数组中的第一位作为中心数进行排序,为了减少交换次数,可以让第一位的数字为自己,中间,末尾三个数中的中位数。
//利用三数取中法对partition进行优化 三数取中保证left位上的数字是三个数字中的中位数
int partitionModified(vector<int> &nums, int left,int right) {
int mid = left + (right - left) / 2;
if (nums[left] > nums[right]) swap(nums[left],nums[right]);
if (nums[mid] > nums[right]) swap(nums[mid], nums[right]);
if (nums[mid] > nums[left]) swap(nums[left], nums[mid]);//left位为中间大小的数
int key = nums[left];
while (left < right) {
while (left < right && key <= nums[right]) --right;
swap(nums[left], nums[right]);
while (left < right && key >= nums[left]) ++left;
swap(nums[left], nums[right]);
}
return left;
}
6、堆排序
构建一个最大堆,每次让最大堆的堆顶与末尾的元素进行交换直至结束;
利用二叉树性质,让数组下标0为堆顶,第i个元素的左子树为2*i+1,右子树为2*i+2;
构建堆的时间复杂度为O(n),重建堆的时间复杂度为O(nlogn),因此该算法时间复杂度为O(nlogn),空间复杂度O(1)
void adjustHeap(vector<int> &nums, int nodeIndex, int len) {
int temp = nums[nodeIndex];//将此时的父节点备份
for (int i = nodeIndex * 2 + 1; i < len; i = i * 2 + 1) {
if (i + 1 < len && nums[i + 1] > nums[i])
++i;//让i指向左右节点中最大的那一个
if (nums[i] <= temp)//如果父节点本身最大就不用再进行交换了
break;
nums[nodeIndex] = nums[i];//让父节点是三个节点中最大的值
nodeIndex = i;//让与父节点交换过值的节点再与其子节点比较
}
nums[nodeIndex] = temp;//最终节点停留的位置赋予父节点之前保留的值
}
void heapSort(vector<int>& nums,int len) {
//int len = nums.size();
for (int i = len / 2; i >= 0; --i) {//父节点都在数组一般之前 从下至上构建最大堆
adjustHeap(nums, i, len);
}
for (int i = len - 1; i > 0; --i) {//将堆顶和最后一个交换
swap(nums[0], nums[i]);
adjustHeap(nums, 0,i-1);
}
}
7、归并排序
归并排序
将数组分成 两个子序列 直到为两个单个元素时间复杂O(n)
采用二分后排序 复杂度logn 因此该排序时间复杂度O(nlogn)
该算法需要临时空间来存放排好序的数组元素,因此该算法空间复杂度为O(n)
void mergeTwoArray(vector<int> &nums, int left,int mid, int right) {
vector<int> temp;//临时数组用来存left和right之间排好序的数
int i = left, j = mid + 1;
int count = 0;//临时数组中的数
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) {
temp.push_back(nums[i++]);
++count;
}
else {
temp.push_back(nums[j++]);
++count;
}
}
while (i <= mid) {
temp.push_back(nums[i++]);
++count;
}
while (j <= mid) {
temp.push_back(nums[j++]);
++count;
}
for (int k = 0; k < count; ++k) {
nums[left + k] = temp[k];
}
}
void mergeArray(vector<int> &nums, int left, int right) {
if (left >= right)
return;
int mid = left + (right - left) / 2;
mergeArray(nums, left, mid);
mergeArray(nums, mid + 1, right);
mergeTwoArray(nums, left, mid, right);
}
8、计数排序
计数排序 计数排序是一个稳定的排序算法。
当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),
其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。*/
算法步骤: 1、开辟一个count数组,记录nums数组中最小元素到最大元素中各个元素出现的个数
2、计算各个元素及其之前元素出现的个数
3、通过个数计算其在原数组中的位置
void countSort(int *nums, int len) {
if (nums == NULL) return;
int min = nums[0], max = nums[0];
for (int i = 1; i < len; ++i) {
max = max > nums[i] ? max : nums[i];
min = min < nums[i] ? min : nums[i];
}
int size = max - min + 1;//确定计数数组的大小
int *count = (int*)malloc(sizeof(int)*size);//count数组
memset(count, 0, sizeof(int)*size);
for (int i = 0; i < len; ++i) ++count[nums[i] - min];
for (int i = 1; i < size; ++i) count[i] += count[i - 1];//计算小于等于i+min的元素的个数
int *copy = (int*)malloc(sizeof(int)*len);
memset(copy, 0, sizeof(int)*len);
for (int i = len - 1; i >= 0; --i) {//升序排列从后往前可以保证稳定性
copy[count[nums[i] - min]-1] = nums[i];//将nums[i]放到正确的位置
--count[nums[i] - min];
}
for (int i = 0; i < len; ++i)
nums[i] = copy[i];
free(count);
free(copy);
count = NULL;
copy = NULL;
}
9、桶排序
桶排序 是将数组与链表的方式结合起来的排序方式
算法步骤:1 构建合理的函数映射关系,将不同元素映射到数组指针指向的桶中 这里时间复杂度O(n)
2 将每个桶中的链表排序,利用头插法
3 将每个排好序的链表连接起来
const int bucketNums = 10;
struct ListNode {
int data;
ListNode* next;
ListNode(int x=0):data(x), next(NULL) {}
};
ListNode* insert(ListNode* head, int val) {
ListNode dummyNode;
dummyNode.next = head;
ListNode* newNode = new ListNode(val);
ListNode* pre = &dummyNode, *cur = head;
//寻找插入点 如果head为空则说明还没有节点放进该桶则直接插入
while (cur != NULL && val >= cur->data) {
pre = cur;
cur = cur->next;
}
//插入新的元素
//ListNode* next = pre->next;
newNode->next = cur;
pre->next = newNode;
return dummyNode.next;
}
//合并链表
ListNode* mergeList(ListNode *head1, ListNode *head2) {
ListNode dummyNode;
ListNode* dummyptr = &dummyNode;
while (head1!= NULL&&head2 != NULL) {
if (head1->data <= head2->data) {
dummyptr->next = head1;
head1 = head1->next;
}
else {
dummyptr->next = head2->next;
head2 = head2->next;
}
dummyptr = dummyptr->next;
}
if (head1!=NULL) dummyptr->next = head1;
if(head2!=NULL) dummyptr->next = head2;
return dummyNode.next;
}
void bucketSort(vector<int>& nums) {
vector<ListNode*> bucket(bucketNums, (ListNode*)(0));
for (int i = 0; i < nums.size(); ++i) {
int index = nums[i] / bucketNums;
ListNode* head = bucket.at(index);//bucket[index];找到对应的桶节点
bucket.at(index) = insert(head, nums[i]);//插入新的节点
}
ListNode* head = bucket[0];//让头结点指向第一个链表
for (int i = 1; i < bucketNums; ++i) {
head = mergeList(head, bucket.at(i));//合并链表
}
for (int i = 0; i < nums.size(); ++i) {
nums[i] = head->data;
head = head->next;
}
}
10、基数排序
基数排序
步骤:1 寻找所有数字中最长的那一个 确定排序轮次
2 LSD从低位开始计算 从低位到高位记录每个数字出现的次数
3 将统计好的次数在临时数组中排序
int getMaxDigits(int nums[], int len) {
int d = 1, r = 10;
for (int i = 0; i < len; ++i) {
while (nums[i] >= r) {
++d;
r *= 10;
}
}
return d;
}
void baseSort(int nums[], int len) {
int d = getMaxDigits(nums, len);
int *temp = new int[len];
int count[10];
int base = 1;
for (int i = 0; i < d;++i) {
int k = 0;
for (int j = 0; j < 10; ++j) {
count[j] = 0;
}
for (int j = 0; j < len; ++j) {
k = (nums[j] / base)% 10;//求最低位
++count[k];
}
for (int j = 1; j < 10; ++j)
count[j] = count[j - 1] + count[j];//计算每个桶及其之前数字的个数
for (int j = len - 1; j >= 0; --j) {
k = (nums[j] / base) % 10;//确定nums[j]放入的桶
temp[count[k] - 1] = nums[j];//将其放进temp中
--count[k];
}
for (int j = 0; j < len; ++j)
nums[j] = temp[j];
base *= 10;
}
delete[]temp;
}
本文详细总结了十大排序算法,包括冒泡排序、选择排序、插入排序、希尔排序、快速排序、堆排序、归并排序、计数排序、桶排序和基数排序,分析了它们的时间复杂度、空间复杂度和稳定性。对于每种排序算法,还介绍了其工作原理和优化策略,是理解排序算法的好资源。

5万+

被折叠的 条评论
为什么被折叠?



