跳过正文
  1. Posts/

链表/单链表 O(nlogn)排序

·1413 字·3 分钟· loading
DogDu
作者
DogDu
相信代码的力量,用实现驱动学习

写单链表的时候我突然想到一个问题:数组有 sort,那链表自己的 O(n log n) 排序范式到底是什么?

顺着这个问题往下查,很自然就会走到 std::list::sort(),也就能看到链表为什么天然适合归并排序。

第一个想法:用quicksort,带头尾指针的双链表确实可以,但是效率不优,因为在选择pivot的时候只能选择最左端,很容易退化成冒泡,更别说单链表了压根就不能用了。

第二个想法:嗯嗯嗯….这种问题肯定前人已经解决过了。所以去看看stl库吧。

用C++ list库,然后写一行 .sort() 再看定义。

CPP 代码 · 共 43 行
template<typename _Tp, typename _Alloc>
    void
    list<_Tp, _Alloc>::
    sort()
    {
      // Do nothing if the list has length 0 or 1.
      if (this->_M_impl._M_node._M_next != &this->_M_impl._M_node
	  && this->_M_impl._M_node._M_next->_M_next != &this->_M_impl._M_node)
      {
        list __carry;
        list __tmp[64];
        list * __fill = __tmp;
        list * __counter;
	__try
	  {
	    do
	      {
		__carry.splice(__carry.begin(), *this, begin());
 		for(__counter = __tmp;
		    __counter != __fill && !__counter->empty();
		    ++__counter)
		  {
		    __counter->merge(__carry);
		    __carry.swap(*__counter);
		  }
		__carry.swap(*__counter);
		if (__counter == __fill)
		  ++__fill;
	      }
	    while ( !empty() ); 
    	for (__counter = __tmp + 1; __counter != __fill; ++__counter)
	      __counter->merge(*(__counter - 1));
	    swap( *(__fill - 1) );
	  }
	__catch(...)
	  {
	    this->splice(this->end(), __carry);
	    for (int __i = 0; __i < sizeof(__tmp)/sizeof(__tmp[0]); ++__i)
	      this->splice(this->end(), __tmp[__i]);
	    __throw_exception_again;
	  }
       }
     }

这里确实有点看不太懂。

关于stl这里的源码解析,推荐:C++ SGI STL 的 list::sort() 分析_list.sort函数 c++-CSDN博客

解析的很清晰,用的是非递归的mergesort,尤其是运用splice函数和counter数组,充分发挥了链表的优势,如果数组用这种方法反而会变慢。

我这里只是用单链表进行了实践,双向链表也是一样的。

首先定义:

CPP 代码 · 共 4 行
typedef struct LIST {
    int data;
    LIST* next;
} *Linklist;

几个辅助函数:

CPP 代码 · 共 35 行
void init_list(Linklist& list) {
    Linklist p = new LIST;
    list = p;
    list->next = NULL;
}

// 尾插法初始化
void init(Linklist& list) {
    int n, x;
    Linklist p = list;

    cin >> n;
    while (n--) {
        cin >> x;

        Linklist temp = new LIST;
        temp->next = NULL;
        temp->data = x;

        p->next = temp;
        p = p->next;
    }
}

void show(Linklist& list) {
    Linklist p = list;

    while (p->next != NULL) {
        cout << p->next->data << ' ';
        p = p->next;
    }

    cout << endl;
    return;
}

然后是stl库中几个辅助函数的时间:

empty():

CPP 代码 · 共 3 行
bool empty(Linklist& p) {
    return p == NULL || p->next == NULL;
}

splice (): 因为只需要用到第一个节点,所以我这里只要了首节点。

CPP 代码 · 共 6 行
void splice_front(Linklist& head, Linklist& p) {
    Linklist temp = head->next;
    head->next = temp->next;
    temp->next = p->next;
    p->next = temp;
}

merge ():

CPP 代码 · 共 22 行
void list_merge(Linklist& list, Linklist& p) {
    Linklist it = list;
    Linklist p1 = list->next;
    Linklist p2 = p->next;

    while (p1 && p2) {
        if (p1->data < p2->data) {
            it->next = p1;
            p1 = p1->next;
        } else {
            it->next = p2;
            p2 = p2->next;
        }

        it = it->next;
    }

    if (!p1) it->next = p2;
    else it->next = p1;

    p->next = NULL;
}

最后是sort部分的函数:

代码块 · 共 36 行
void list_sort(Linklist& list) {
    if (list->next == NULL || list->next->next == NULL) return;

    Linklist carry = new LIST;
    Linklist counter[64];
    for (int i = 0; i < 64; ++i) {
        counter[i] = new LIST;
        counter[i]->next = NULL;
    }
    carry->next = NULL;

    int fill = 0;
    while (!empty(list)) {
        splice_front(list, carry);
        int i = 0;

        while (i < fill && !empty(counter[i])) {
            list_merge(counter[i], carry);
            swap(carry, counter[i++]);
        }

        swap(carry, counter[i]);
        if (i == fill) ++fill;
    }

    for (int i = 1; i < fill; ++i)
        list_merge(counter[i], counter[i - 1]);

    list->next = counter[fill - 1]->next;

    for (int i = 0; i < 64; ++i)
        delete counter[i];

    delete carry;
    return;
}

全部代码:

CPP 代码 · 共 118 行
#include <iostream>
#include <list>

#define ll long long

using namespace std;

typedef struct LIST {
    int data;
    LIST* next;
} *Linklist;

void init_list(Linklist& list) {
    Linklist p = new LIST;
    list = p;
    list->next = NULL;
}

// 尾插法初始化
void init(Linklist& list) {
    int n, x;
    Linklist p = list;

    cin >> n;
    while (n--) {
        cin >> x;

        Linklist temp = new LIST;
        temp->next = NULL;
        temp->data = x;

        p->next = temp;
        p = p->next;
    }
}

void show(Linklist& list) {
    Linklist p = list;

    while (p->next != NULL) {
        cout << p->next->data << ' ';
        p = p->next;
    }

    cout << endl;
    return;
}

bool empty(Linklist& p) {
    return p == NULL || p->next == NULL;
}

void splice_front(Linklist& head, Linklist& p) {
    Linklist temp = head->next;
    head->next = temp->next;
    temp->next = p->next;
    p->next = temp;
}

void list_merge(Linklist& list, Linklist& p) {
    Linklist it = list;
    Linklist p1 = list->next;
    Linklist p2 = p->next;

    while (p1 && p2) {
        if (p1->data < p2->data) {
            it->next = p1;
            p1 = p1->next;
        } else {
            it->next = p2;
            p2 = p2->next;
        }

        it = it->next;
    }

    if (!p1) it->next = p2;
    else it->next = p1;

    p->next = NULL;
}

void list_sort(Linklist& list) {
    if (list->next == NULL || list->next->next == NULL) return;

    Linklist carry = new LIST;
    Linklist counter[64];
    for (int i = 0; i < 64; ++i) {
        counter[i] = new LIST;
        counter[i]->next = NULL;
    }
    carry->next = NULL;

    int fill = 0;
    while (!empty(list)) {
        splice_front(list, carry);
        int i = 0;

        while (i < fill && !empty(counter[i])) {
            list_merge(counter[i], carry);
            swap(carry, counter[i++]);
        }

        swap(carry, counter[i]);
        if (i == fill) ++fill;
    }

    for (int i = 1; i < fill; ++i)
        list_merge(counter[i], counter[i - 1]);

    list->next = counter[fill - 1]->next;

    for (int i = 0; i < 64; ++i)
        delete counter[i];

    delete carry;
    return;
}

去洛谷试试吧:P1177 【模板】排序 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

用时104ms

内存3.67MB

内存逼近O(logn)但是list的空间密度较低,所以内存较大,时间和数组的quicksort逼近了。让我怀疑在数组中 nlogn 的排序方法中mergesort之所以是 n 的空间复杂度,就是因为它实际上是为list设计的。