(一)题目要求:
用插入排序对链表排序
(二)示例:
Given 1->3->2->0->null
, return 0->1->2->3->null
(三)题解:
(1)插入排序简介:
插入排序的核心思想就是把待排序的序列分为两部分(已排序,未排序):
(a)最开始已排序部分只有一个元素,当然是有序的;
(b)向后扫描未排序部分的第一个元素,将其插入到已排序部分的合适位置(方法是逐个与已排序部分的元素比较)
(c)以此类推
时间复杂度:O(n^2) 稳定排序
这里参考几个资料(不仅有插入排序,还有选择排序,冒泡排序的介绍):
http://blog.csdn.net/llzk_/article/details/51628574
http://blog.csdn.net/lemon_tree12138/article/details/50968422
https://www.cnblogs.com/Kevin-mao/p/6013452.html
(2)注意:
(a)严格按照定义来说,将未排序部分的元素插入时,需从后往前扫描,然后将大于元素值(假设是从小到大排序)依次后移,找到插入位置后跳出循环,插入元素。
(b)由于这里是对单链表排序,故无法从后往前扫描,另外链表这种结构决定了没必要进行依次后移这种操作,操作指针即可。严格按照定义的方法更适用于对数组进行排序。
(c) 一点体会:
其实排序最好的类比方法就是按照大小个排队,插入排序的方法就是逐个拿后边的人与前边已排好队的人进行比较,找到合适的位置插入。
从前往后比时,这个合适的位置是高于他的人前边那个位置(如果所有人都比他矮,则他的位置不变)
当然严格的定义是:
(1)从后往前比(为什么不从前往后比,可以想象这个队伍过于长,要把这个人从他的位置走到开头的位置耗费了不必要的时间;另外可以想象,假设队伍的大小个已经大致排好,这时候后边未排序的人高于前边的人的概率是较高的,这时候从后往前比显然效率要更高些(从这点可以想象,在什么情况下用插入排序效率会更高,即原序列已大致按照大小排好));
引:关于未什么从后向前遍历,在学过了线性表之后有了更合适的解释:
实际这还是由线性表的顺序存储结构决定的(数组),顺序存储结构在
插入元素时:应该从最后一个元素向前遍历,以简化操作
删除元素时:应该从所删除的元素向后遍历,以简化操作
(2)然后让高于他的人依次后移给这个人让地儿,找到比他矮的人或走到了队伍的开头,就将这个人安排在此位置;同样,如果所有人都比他矮,则他的位置不变(注意由于前边已经排好了大小个,所以这个人如果越高,进行比较的次数也就越少)。
方法一:
这个是我自己参照定义写的方法,对于链表的处理依旧引入了虚拟头结点,使对头结点的处理与其他结点一致,无需单独讨论,
lintcode测试通过:
/**
* Definition of ListNode
* class ListNode {
* public:
* int val;
* ListNode *next;
* ListNode(int val) {
* this->val = val;
* this->next = NULL;
* }
* }
*/
//插入排序:
//将链表分为已排序和未排序两部分
//逐步向后扫描将未排序部分的元素插入已排序部分
//时间复杂度 O(n^2)
class Solution {
public:
/*
* @param head: The first node of linked list.
* @return: The head of linked list.
*/
ListNode * insertionSortList(ListNode * head) {
// write your code here
if(!head || !head->next)
return head;
ListNode Dummy(0);
Dummy.next = head;
bool is_inserted = false;
//外层循环为扫描原链表,内层循环比较插入结点与已排序部分链表的值
for(ListNode *p_current = head,*p_insert = p_current->next;p_insert!=NULL;p_insert=p_current->next)
{
for(head = &Dummy;head->next != p_insert;head = head->next)
{
if(p_insert->val<head->next->val)
{
p_current->next = p_insert->next;
p_insert->next = head->next;
head->next = p_insert;
is_inserted = true;
break;
}
}
if(!is_inserted)
p_current = p_current->next;
else
is_inserted = false;
}
return Dummy.next;
}
};
方法二:
网上找的一个方法,思想是一致的,但把有序部分单独建立一个链表,好处是程序可读性更好:
//方法二:思想和方法一 一致,但引入了新的链表作为插入结点的链表(新的链表作为已排序的链表),使程序可读性更高
class Solution {
public:
/**
* @param head: The first node of linked list.
* @return: The head of linked list.
*/
ListNode *insertionSortList(ListNode *head) {
ListNode *dummy = new ListNode(0);
// 这个dummy的作用是,把head开头的链表一个个的插入到dummy开头的链表里
// 所以这里不需要dummy->next = head;
while (head != NULL) {
ListNode *temp = dummy;
ListNode *next = head->next;
while (temp->next != NULL && temp->next->val < head->val) {
temp = temp->next;
}
head->next = temp->next;
temp->next = head;
head = next;
}
return dummy->next;
}
效率不高的一个方法:
这里有一个效率不高的方法,是我在刚开始拿到这道题时,由于还不是很理解插入排序,写出的一个方法,当然他不是错的,运行可以得到正确结果;这种方法最严重的问题就是把链表当做数组来处理,在插入待排序元素后,做不必要的元素依次后移:
//该方法实际不是插入排序,是变形了的选择排序??
//浪费感情的方法:找到插入位置后,实际插入即可,无需再继续比较(并且该方法为不稳定排序)
//既缺心眼儿又不稳定(时间复杂度大)O(n^2)
//如 3(1)->3(2)->4->2
//----->2->3(2)->4->3(1)
//----->2->3(2)->3(1)->4
class Solution {
public:
/*
* @param head: The first node of linked list.
* @return: The head of linked list.
*/
ListNode * insertionSortList(ListNode * head) {
// write your code here
if(!head)
return NULL;
for(ListNode *p_cmp = head->next;p_cmp!=NULL;p_cmp = p_cmp->next)
for(ListNode *p_loc = head;p_loc != p_cmp;p_loc = p_loc->next)
{
if(p_cmp->val<p_loc->val)
{
int tmp = p_cmp->val;
p_cmp->val = p_loc->val;
p_loc->val = tmp;
}
}
return head;
}
};
仔细分析该方法为何效率低,且不合适:
(1)该方法同样是从前向后扫描 ,将未排序部分的值与已排序部分比较,但是在比较时采取了交换元素值的方法,没有利用链表这种数据结构的特性。
(2)该方法是不稳定的。可能改变原有元素的相对位置。
同样用排队模型来类比,可以看出这种方法有多缺心眼儿:
从前往后,将未排队的人和前边的人比较(前边的人按大小个排好),如果找到比他高的人,则将这个人安排在此位置(这点没错),错在后边,由于是交换值而不是插入这个位置,所以需要将交换出来的这个人继续和后边的人比较,然后在明知后边人都比他高的情况下继续不断的重复插入,抽出一个新人,再比较,插入这个过程(实际上这个过程就是依次将后边的元素后移的过程,将链表当做数组来处理,没有利用链表方便插入删除元素的特性,反而受限于单链表只能向后遍历,使后移操作更复杂)。。。显然,现实中没有人会这么去排队,可以想象在队伍很长的情况下,这种方法会浪费多少不必要的时间。
由此可以看出,编程实际源于生活,最终也是要解决现实中的问题,实际只是对客观世界的事情进行抽象后,对数据进行操作(算法),我感觉,说不好当初发明插入排序这个方法的人就是参考排队这件事而给出的(只是猜测,毕竟太像了)。
有时候要将抽象的对数据的操作具体化,就知道一些错误有多么愚蠢。。。
最后,学会用程序去解决问题,而不是带来问题,有的时候东西学多了,学的陷进去,也就学傻了;语言和算法,数据结构产生于前人大量的实践,而新的方法的产生往往源于生活中的其他领域,而不是局限于程序。学习编程,也只有通过实践遇到问题,才能掌握这些,知道为什么会产生这个方法。。。
以上。。。