链表之排序(插入、选择、归并、快速、冒泡)

已知链表节点结构如下:

struct ListNode {

  int val; //数值

  ListNode *next; //后继指针

  ListNode(int x) : val(x), next(NULL) {}

  };


插入排序算法交换节点,思想是先构造一个fakeHead让其指向head,以便于接下来搜索待插入节点应该插入的地方。cur指针指向待插入节点,pre为cur指针的前驱,next为cur指针的后继。时间复杂度为O(N^2),空间复杂度为O(1)。更详细的算法思想,见代码注释:

class Solution {
public:
    ListNode* insertionSortList(ListNode* head) {
        if(head==NULL||head->next==NULL)
            return head;
        //cur初始化为指向第二个元素,pre初始化为指向head, next初始化为NULL
        ListNode* fakeHead=new ListNode(0), *p, *cur=head->next, *next=NULL, *pre=head;
        fakeHead->next=head; //使fakeHead指向head,以便于接下来搜索待插入节点应该插入的位置
        while (cur) {
            next=cur->next; //保存cur的后继next以便于接下来的操作
            if (cur->val>=pre->val) { //如果cur节点的数值>=pre节点的数值,则说明cur节点,不需要移动。
                pre=cur; //更新pre指针
            } else {
                pre->next=next; //移动cur节点之前,让pre和next连接起来
                p = fakeHead; 
                while (p->next->val<cur->val) { //搜索cur节点插入的位置,cur应该插入p节点之后
                    p = p->next;
                }
                cur->next=p->next; //连接cur和p指针后面的节点
                p->next=cur; //连接p和cur指针
            }
            cur=next; //更新cur指针
        }
        p = fakeHead->next;
        delete fakeHead; //delete fakeHead
        return p;
    }
};

选择排序算法交换节点的值,算法复杂度为O(N^2),空间复杂度为O(1),思想是先构造一个伪头结点,让其指向head,以便于操作。另外,用sortedTail指针指向有序部分的末尾,剩下的工作就是在无序部分找数值最小的节点minNode,若minNode不等于sortedTail->next,则交换sortedTail->next节点和minNode节点的数值。更详细的思想,见代码注释:

ListNode* selectSortList(ListNode *head) {
    if(head==NULL)
        return head;
    ListNode* fakeHead = new ListNode(0);
    fakeHead->next = head; //为了操作方便,添加伪头节点
    ListNode* sortedTail = fakeHead; //sortedTail指向已排序部分的尾部,注意在链表中的这种使用方法
    
    while(sortedTail->next != NULL)
    {
        //minNode记录最小节点
        ListNode* minNode = sortedTail->next, *p = sortedTail->next->next;
        
        //寻找未排序部分的最小节点
        while(p != NULL)
        {
            if(p->val < minNode->val)
                minNode = p;
            p = p->next;
        }
        if(minNode != sortedTail->next)
            swap(minNode->val, sortedTail->next->val); //交换节点数值
        sortedTail = sortedTail->next;
    }
    head = fakeHead->next;
    delete fakeHead;
    return head;
}

归并排序算法交换链表节点,时间复杂度为O(NlogN),不考虑递归栈空间的话空间复杂度是O(1)) ,算法思想是首先用快慢指针的方法找到链表中间节点,然后递归地对两个子链表进行排序,把两个排好序的子链表合并成一条有序的链表。归并排序算是链表排序中的最好选择,保证了最好和最坏时间复杂度都是NlogN,而且它在数组排序中广受诟病的空间复杂度在链表排序中也从O(N)降到了O(1)。

ListNode* merge(ListNode* left, ListNode* right) {
    if(left==NULL) { //如果一个为空,则直接返回另外一个
        return right;
    } else if (right==NULL) {
        return left;
    }
    ListNode* res, *tmpPos;
    if (left->val<right->val) { //确定头指针
        res=left;
        left=left->next;
    } else {
        res = right;
        right=right->next;
    }
    tmpPos=res;
    while (left!=NULL&&right!=NULL) {
        if(left->val<right->val) {
            tmpPos->next=left;
            left=left->next;
        } else {
            tmpPos->next=right;
            right=right->next;
        }
        tmpPos=tmpPos->next;
    }
    if (left!=NULL) {
        tmpPos->next=left;
    }
    if (right!=NULL) {
        tmpPos->next=right;
    }
    return res; //返回已经合并好的链表的头指针
    
}
ListNode* mergeSortList(ListNode *head) {
    if (head==NULL||head->next==NULL) { //递归终止条件
        return head;
    }
    ListNode* fast=head, *slow=head; //通过快慢指针,寻找链表中点
    while (fast->next!=NULL&&fast->next->next!=NULL) {
        fast=fast->next->next;
        slow=slow->next;
    }
    fast=slow->next; //fast为右半部分链表的起始
    slow->next=NULL; //slow为为左半部分的结尾
    
    slow=mergeSortList(head); //对左半部分排序
    fast=mergeSortList(fast); //对右半部分排序
    
    return merge(slow, fast); //合并左右部分
}

快速排序1(算法只交换节点的val值,平均时间复杂度O(nlogn),不考虑递归栈空间的话空间复杂度是O(1))

这里的partition我们参考数组快排partition的第二种写法(选取第一个元素作为枢纽元的版本,因为链表选择最后一元素需要遍历一遍),具体可以参考here这里我们还需要注意的一点是数组的partition两个参数分别代表数组的起始位置,两边都是闭区间,这样在排序的主函数中:

void quicksort(vector<int>&arr, int low, int high)
{
  if(low < high)
  {
   int middle = mypartition(arr, low, high);
   quicksort(arr, low, middle-1);
   quicksort(arr, middle+1, high);
  }
}
对左边子数组排序时,子数组右边界是middle-1,如果链表也按这种两边都是闭区间的话,找到分割后枢纽元middle,找到middle-1还得再次遍历数组, 因此链表的partition采用前闭后开的区间(这样排序主函数也需要前闭后开区间) ,这样就可以避免上述问题。

ListNode* partition(ListNode* low, ListNode* high) {
    int key = low->val;
    ListNode* loc = low; //loc为小于key节点序列的最后一个节点
    for (ListNode* i=low->next; i!=high; i=i->next) {
        if (i->val<key) {
            loc=loc->next;
            swap(i->val, loc->val);
        }
    }
    swap(loc->val, low->val);
    return loc; //loc就是枢纽节点
}
void qSortList(ListNode* head, ListNode* tail) {
    if(head!=tail&&head->next!=tail) { //如果子链表中至少有两个元素
        ListNode* mid = partition(head, tail); //得到枢纽节点
        qSortList(head, mid);
        qSortList(mid->next, tail);
    }
}
ListNode* quickSortList(ListNode* head) {
    if (head==NULL||head->next==NULL) {
        return head;
    }
    qSortList(head, NULL);
    return head;
}

快速排序2(算法交换链表节点,平均时间复杂度O(nlogn),不考虑递归栈空间的话空间复杂度是O(1))

这里的partition,我们选取第一个节点作为枢纽元,然后把小于枢纽的节点放到一个链中,把不小于枢纽的及节点放到另一个链中,最后把两条链以及枢纽连接成一条链。

这里我们需要注意的是,1.在对一条子链进行partition时,由于节点的顺序都打乱了,所以得保正重新组合成一条新链表时,要和该子链表的前后部分连接起来,因此我们的partition传入三个参数,除了子链表的范围(也是前闭后开区间),还要传入子链表头结点的前驱;2.partition后链表的头结点可能已经改变

ListNode* partition(ListNode* lowPre, ListNode* low, ListNode* high) {
    int key = low->val;
    ListNode node0(0), node1(0);
    ListNode* little=&node0, *big=&node1;
    for (ListNode* i=low->next; i!=high; i=i->next) {
        if (i->val<key) {
            little->next=i;
            little=little->next;
        } else {
            big->next=i;
            big=big->next;
        }
    }
    big->next=high; //保证子链表[low, high)和后面的部分连接
    little->next=low;
    low->next=node1.next;
    lowPre->next=node0.next; //保证子链表[low, high)和前面的部分连接
    return low;
    
}
void qSortList(ListNode* headPre, ListNode* head, ListNode* tail) {
    if(head!=tail&&head->next!=tail) { //如果子链表中至少有两个元素
        ListNode* mid = partition(headPre, head, tail); //注意这里的head可能不再指向表头了
        qSortList(headPre, headPre->next, mid);
        qSortList(mid, mid->next, tail);
    }
}
ListNode* quickSortList(ListNode* head) {
    if(head==NULL||head->next==NULL)
        return head;
    ListNode* headPre = new ListNode(0);
    headPre->next=head;
    qSortList(headPre, head, NULL);
    return headPre->next;
}

冒泡排序(算法交换链表节点val值,时间复杂度O(n^2),空间复杂度O(1))

class Solution {
public:
    ListNode *bubbleSortList(ListNode *head) {
        // IMPORTANT: Please reset any member data you declared, as
        // the same Solution instance will be reused for each test case.
        //链表快速排序
        if(head == NULL || head->next == NULL)return head;
        ListNode *p = NULL;
        bool isChange = true;
        while(p != head->next && isChange)
        {
            ListNode *q = head;
            isChange = false;//标志当前这一轮中又没有发生元素交换,如果没有则表示数组已经有序
            for(; q->next && q->next != p; q = q->next)
            {
                if(q->val > q->next->val)
                {
                    swap(q->val, q->next->val);
                    isChange = true;
                }
            }
            p = q;
        }
        return head;
    }
};

对于希尔排序,因为排序过程中经常涉及到arr[i+increment]操作,其中increment为希尔排序的当前步长,这种操作不适合链表。

对于堆排序,一般是用数组来实现二叉堆,当然可以用二叉树来实现,但是这么做太麻烦,还得花费额外的空间构建二叉树

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值