对于链表的排序算法,除了希尔排序之外,且堆排序不建议,其他排序方法都是支持的,如下:
冒泡排序,选择排序,插入排序,归并排序,快速排序,计数排序,桶排序和基数排序。
下面废话不多说,直接开始排序
定义一个链表的结构
这里我定义一个简单的链表节点(头节点不包含数据),如下所示
typedef struct Node{
int data;
Node* next;
Node():data(0),next(NULL){};
Node(int x):data(x),next(NULL){};
}*Head;
//添加节点
void AddNode(Head head,Node* node){
node->next = head->next;
head->next = node;
}
//输出
#define for_each(pow, head) \
for(pow = head->next; pow != NULL; pow = pow->next)
这里我们首先进行初始化,以及添加一些节点。
int main(void){
Head list_head = new Node(); //头节点
Node* node = NULL;
node = new Node(3);
AddNode(list_head, node);
node = new Node(2);
AddNode(list_head, node);
node = new Node(8);
AddNode(list_head, node);
node = new Node(4);
AddNode(list_head, node);
node = new Node(5);
AddNode(list_head, node);
Node* pow=NULL;
for_each(pow, list_head){
cout<<pow->data<<endl;
}
system("pause");
return 0;
}
经过测试,对于链表初始化,添加节点,以及遍历显示都正常,现在开始尝试我们的排序算法。
冒泡排序
- 算法思想:每次比较相邻元素之间的大小,使值小的逐步从后面移动到前面,值大的从前面移到后面。
- 时间复杂度:
- 最好时间复杂度:O(n)
- 最坏时间复杂度:O(n2)
- 排序稳定性:由于元素交换是在相邻元素之间进行的,不会改变值相同元素的相对位置,因此,冒泡排序法是一种 稳定排序算法。
void BubbleSort(Head &head){
Node* i = head->next;
Node * tail = NULL;
for(; i->next != NULL; i = i->next){
Node* j = head->next;
for(; j->next != tail; j = j->next){
if(j->data > j->next->data){
int temp = j->data;
j->data = j->next->data;
j->next->data = temp;
}
}
tail = j; //每次向前移动一位
}
}
冒泡排序的链表实现和数组是一样的,只不过需要三个指针进行两个for循环,最后一个指针tail
用于指向j这个指针的末尾,每次循环,则-1。
选择排序
- 算法思想:每次从未排序的元素中找到最小的值,并与未排序最前面的元素交换位置。
- 时间复杂度:因为排序比较次数与序列的原始状态无关,总是O(n2)
- 排序稳定性:因为交换不是相邻两元素,可能会改变值相同元素的前后位置,故是一种不稳定的排序算法
C++实现
void SectionSort(Head &head){
Node* i = head->next;
for(; i->next != NULL; i = i->next){
Node* j = head->next;
Node* min_node=i->next;
for(j = i; j != NULL; j = j->next){
if(min_node->data > j->data){
min_node = j;
}
}
if(i != min_node){
Swap(i,min_node);
}
}
}
这次只需要两个指针,每次从i开始遍历到结尾,找出最小的数据节点,将其与i的位置进行调换,然后“i++”。
插入排序
- 算法思想:每次将无序列表中的元素插入到有序列表中。
- 时间复杂度:
- 最佳时间复杂度:O(n)
- 最差时间复杂度:O(n2)
- 平均时间复杂度:O(n2)
- 排序稳定性:稳定排序算法
C++实现
void InsertSort(Head &head){
Node* i = head->next;
if(i == NULL) return;
else if(i->next == NULL) return;
Node* sort_list=head->next; //第一个节点
i = i->next;
while(i){ //第二个节点开始
if(sort_list->data <= i->data){
sort_list = sort_list->next; //有序的则移动
}
else{
Node* p1 = head; //从有序链表开始遍历
while(p1->next->data <= i->data){
p1 = p1->next;
}
// 插入操作
sort_list->next = i->next;
i->next = p1->next;
p1->next = i;
}
i = sort_list->next;
}
}
插入排序的实现,相比之数组,要复杂许多。数组的插入排序,可以在插入数据与有序数组比较的过程中完成位移覆盖,这取决于数组的删除特性。而链表则不行,实现过程由三个指针。
sort_list指针用来维护有序列表的尾部。i指针用来表示无序区域的值,而这里我们的头节点作为插入排序比较的指针。
- 比较有序列表尾部与无序列表的首部的值,如果无序列表首部大,则有序列表的sort_list指针向前移动。
- 如果无序列表首部比有序列表的尾部要小,那么就需要进行插入操作。这里p1指针从头部开始遍历,直到找到i应该插入的地方。
- 然后进行三个指针指向的操作,完成插入步骤。
归并排序
- 算法思想:不断的分割数据,直到数据不能在被分割,在通过归并的思想,将数据合并起来。
- 时间复杂度:
- 时间复杂度:O(n×log2n)。
- 空间复杂度:O(1)。
- 排序稳定性:由于元素交换是在相邻元素之间进行的,不会改变值相同元素的相对位置,因此,冒泡排序法是一种 稳定排序算法。
C++实例
list_head->next = MergeSort(list_head->next);
for_each(pow, list_head) cout<<pow->data<<"\t"; cout<<endl;
这段是测试代码,因为归并的时候,会将数据进行分割,不适合头节点无数据,这里直接使用list_head->next作为参数传入数据。
Head MergeSort(Head head){
if(NULL == head || NULL == head->next) return head;
// 快慢指针找到中心链节点
Node* slow=head;
Node* fast=head->next;
while(fast && fast->next){
slow = slow->next;
fast = fast->next->next;
}
// 得到两个链表
Node* left_head = head;
Node* right_head = slow->next;
slow->next = NULL;
// 归并操作
return Merge(MergeSort(left_head), MergeSort(right_head));
}
归并排序操作如下:
- 判断第一个节点和第二个节点是否有数据,没有则直接返回。
- 使用快慢指针找到中间的节点
- 将其划分为左链表和右链表
- 我们将左边和右边的链表通过MergeSort排序好,之后在进行归并操作。
因为采用的递归的思想,你可以理解为Merge函数中的左边和右边已经通过MergeSort排序好了。
然后是归并函数的实现
Head Merge(Node* left, Node* right){
Head head = new Node();
Node* cur = head;
// 分离双指针进行数组合并
while(left && right){
if( left->data <= right->data){
cur->next = left;
left = left->next;
}else{
cur->next = right;
right = right->next;
}
cur = cur->next;
}
// 清理剩余的链表
if(left) cur->next = left;
else if(right) cur->next = right;
return head->next;
}
这里主要采用了分离双指针的操作,较为简单。
- 将左边和右边的数据通过两个指针索引进行比较,因为是排好顺序的,所以只需要不断移动指针即可。
- 最后将还没有使用完的剩余链表直接添加,就结束了。
链表快速排序
- 算法思想:随机取序列中的一个值,并将序列以此值为界限,一分为二,并递归进行,直到整个序列有序
- 时间复杂度:
- 时间复杂度:O(n×log2n)。
- 空间复杂度:O(1)。
- 排序稳定性:不稳定排序算法。
C++实现
Head QuickSort(Node* head){
if(NULL == head->next || NULL == head->next->next) return head;
head->next = Quick(head->next, NULL);
return head;
}
也是同样的,因为头节点没有数据,所以这里先将头节点提取出来之后,在进行排序。
Node* Quick(Node* left, Node* right){
// 基线条件
if(left == right || left->next == right) return left;
//left链表的值被更新,且得到分割点地址
Node* pi = Partition(left, right);
Quick(left, pi);
Quick(pi->next, right);
return left;
}
同样,这里采用递归的方式进行排序算法。
- 首先是基线条件,当从左到右,只有0或者1个节点的时候,返回该分段的起始地址left。
- Partition函数的作用是选取头节点作为基准值,然后对链表的值进行分割。
- 方式和数组有异曲同工之妙,这里只对数据进行了修改,没有对节点进行修改。
- 然后递归对left到pi再次进行快排。
这里巧妙之处在于,虽然链表导致数据存储的物理位置不是连续的,但是链表的地址可以看成是连续的,这里我们通过链表的地址,来分割左边和右边。
计数排序
- 算法思想:使用一个范围涵盖所有数据的额外数组统计原数组中元素出现的个数,在排到正确的顺序
- 时间复杂度:O(n+k),其中 k 代表待排序链表中所有元素的值域。
- 空间复杂度:O(k)。
- 排序稳定性:稳定排序算法。
C++实现
Head CountingSort(Head head){
if(NULL == head->next || NULL == head->next->next) return head;
// 获取最大值和最小值
int list_max = INT_MIN,list_min = INT_MAX;
Node* pow = head->next;
while(pow){
if(pow->data > list_max) list_max = pow->data;
if(pow->data < list_min) list_min = pow->data;
pow = pow->next;
}
// 初始化数组
const int size = list_max-list_min+1;
int counts[size] = {0};
// 计数
pow = head->next;
while(pow){
counts[pow->data - list_min] += 1;
pow = pow->next;
}
pow = head;
for(int i=0; i<size; i++){
while(counts[i]){
pow->next->data = i+list_min;
counts[i] -= 1;
pow = pow->next;
}
}
return head;
}
计数排序原理比较简单,因为引入了额外的空间,操作更加的简便。
- 判断链表中节点的数量
- 找出最小值和最大值,生成counts数组
- 对链表中的数进行计数
- 将计数的结果在返回给链表。
桶排序
- 算法思想:
- 时间复杂度:
- 时间复杂度:O(n)。
- 空间复杂度:O(n+m)。m 为桶的个数。
- 空间复杂度:O(n+m)
- 排序稳定性:稳定排序算法。
C++实现
void Insertion(vector<Head> &buckets,int index, int val){
if(!buckets[index]){
buckets[index] = new Node(val);
return;
}
Node* node = new Node(val);
node->next = buckets[index];
buckets[index] = node;
}
插入函数的作用就是,将链表中的所有数据分别放到各个桶里面。
Head bucketSort(Head head, int bucket_size=20){
if(NULL == head->next || NULL == head->next->next) return head;
//找出最大值和最小值
int list_max = INT_MIN,list_min = INT_MAX;
Node* pow = head->next;
while(pow){
if(pow->data > list_max) list_max = pow->data;
if(pow->data < list_min) list_min = pow->data;
pow = pow->next;
}
int bucket_count = (list_max-list_min) / bucket_size + 1;
vector<Head> buckets(bucket_count);
cout << "bucket_count:" << bucket_count <<endl;
// 链表数据分组
Node* cur = head->next;
while(cur){
int index = (cur->data - list_min) / bucket_size;
Insertion(buckets, index, cur->data);
cur = cur->next;
}
// 桶内采用归并排序
cur = head;
for(int i=0; i<buckets.size(); i++){
Head list= MergeSort(buckets[i]);
while(list){
cur->next = list;
cur = cur->next;
list = list->next;
}
}
return head;
}
桶排序其核心思想就是分桶,有点分而治之的思想。然后在对桶中的数据进行各种排序方法。
- 找出最大值,最小值,然后根据bucket_size来进行分桶
- 将链表中的数据分桶装入其中
- 每个桶执行各自的排序算法,最后在进行合并
基数排序
- 算法思想:将整数按位数切割成不同的数字,然后按每个位数分别比较进行排序
- 时间复杂度:O(n*k),k表示位数
- 空间复杂度:O(n+k)
- 排序稳定性:稳定排序算法。
C++实现
int Pow(int a, int i){
int res = 1;
while(i){
res *= a;
i--;
}
return res;
}
简单实现了一个幂次方函数,不错也可以直接调cmath库中的内容
Head RadixSort(Head head){
int size = 0;
Node* cur = head->next;
while(cur){
int val_len = std::to_string(cur->data).size();
size = val_len>size?val_len:size;
cur=cur->next;
}
for(int i=0; i<size; i++){
vector<vector<int>> buckets(10);
cur = head->next;
while(cur){
buckets[cur->data/Pow(10,i)%10].push_back(cur->data);
cur = cur->next;
}
// 生成新表
cur = head;
for(int i=0; i<buckets.size(); i++){
for(int j=0; j<buckets[i].size(); j++){
cur->next = new Node(buckets[i][j]);
cur = cur->next;
}
}
}
return head;
}
- 找出最大值的位数。
- 遍历每一位,并将数据分桶装载。
- 将分桶的数据,按照顺序返回给head。
总结文章来源:https://www.toymoban.com/news/detail-435327.html
这9中排序在实现难度上比之前的数组还困难许多,不过同时也收获许多。文章来源地址https://www.toymoban.com/news/detail-435327.html
到了这里,关于C++链表的排序实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!