Shared posts

08 Jul 01:00

可视化对比十多种排序算法(C#版)

by smilesisi

在这篇文章中,我会向大家展示一些排序算法的可视化过程。我还写了一个工具,大家可对比查看某两种排序算法。

引言

首先,我认为是最重要的是要理解什么是“排序算法”。根据维基百科,排序算法(Sorting algorithm)是一种能将一串数据依照特定排序方式进行排列的一种算法。最常用到的排序方式是数值顺序以及字典顺序。有效的排序算法在一些算法(例如搜索算法与合并算法)中是重要的,如此这些算法才能得到正确解答。排序算法也用在处理文字数据以及产生人类可读的输出结果。

接下来,我会说明一些算法。所有算法皆由C#代码实现,大部分的算法思想都可以在维基百科上找到。 所呈现的算法有:

  • 双向冒泡排序
  • 冒泡排序
  • 桶排序
  • 梳排序
  • 循环排序
  • 地精排序
  • 堆排序
  • 插入排序
  • 归并排序
  • 奇偶排序
  • 鸽笼排序
  • 快速排序
  • 使用冒泡的快排
  • 选择排序
  • 希尔排序

我已经决定要创建GUI可视化的排序算法。该项目还允许用户保存为GIF图像及设置动画输出排序速度。

使用代码

该解决方案由两个项目组成。第一个项目称为组件提供的创建GIF动画图像类。该项目是基于NGIF项目的。关于这个项目的更多信息可以在这里找到

第二个项目可以称为排序比较,它是解决方案的主要组成部分。其中,通过一个名为frmMain的结构可以选择排序算法,设置你想要排序,排序的速度,排序数量,并选择是否要创建动态图片。在窗体上放置两个面板称为pnlSort1和pnlSort2,其中分拣可视化的呈现方式。

每个算法都都通过自己的排序方式进行命名,并接受一个IList参数,并返回一个IList对象。

DrawSamples方法可以在面板上进行绘图。产生的随机样本之后就会调用它。通过点击随机按钮生成的样本会保存在数组中。

private void DrawSamples()
{
    g.Clear(Color.White);

    for (int i = 0; i < array.Count; i++)
    {
        int x = (int)(((double)pnlSamples.Width / array.Count) * i);

        Pen pen = new Pen(Color.Black);
        g.DrawLine(pen, new Point(x, pnlSamples.Height), 
          new Point(x, (int)(pnlSamples.Height - (int)array[i])));
    }
}

该方法随机产生数据放于数组中。

public void Randomize(IList list)
{
    for (int i = list.Count - 1; i > 0; i--)
    {
        int swapIndex = rng.Next(i + 1);
        if (swapIndex != i)
        {
            object tmp = list[swapIndex];
            list[swapIndex] = list[i];
            list[i] = tmp;
        }
    }
}

在排序过程中,当复选框创建的动画被选中,数组中两个数交换的话就会产生图像。此图像索引从0到n,其中n代表交换的次数。

private void SavePicture()
{
    ImageCodecInfo myImageCodecInfo = this.getEncoderInfo("image/gif"); 
    EncoderParameter myEncoderParameter = new EncoderParameter(
      System.Drawing.Imaging.Encoder.Compression, (long)EncoderValue.CompressionLZW);
    EncoderParameter qualityParam = 
      new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 0L);
    EncoderParameters myEncoderParameters = new EncoderParameters(1);

    EncoderParameters encoderParams = new EncoderParameters(2);
    encoderParams.Param[0] = qualityParam;
    encoderParams.Param[1] = myEncoderParameter;

    string destPath = 
      System.IO.Path.Combine(txtOutputFolder.Text, imgCount + ".gif");
    bmpsave.Save(destPath, myImageCodecInfo, encoderParams);
    imgCount++;
}

排序算法

冒泡排序

冒泡排序也被称为下沉排序,是一个简单的排序算法,通过多次重复比较每对相邻的元素,并按规定的顺序交换他们,最终把数列进行排好序。一直重复下去,直到结束。该算法得名于较小元素“气泡”会“浮到”列表顶部。由于只使用了比较操作,所以这是一个比较排序。

冒泡排序算法的运作如下:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

由于它的简洁,冒泡排序通常被用来对于程序设计入门的学生介绍算法的概念。但同样简单的插入排序比冒泡排序性能更好,所以有些人认为不需要再教冒泡排序了。

 

public IList BubbleSort(IList arrayToSort)
{
    int n = arrayToSort.Count - 1;
    for (int i = 0; i < n; i++)
    {
        for (int j = n; j > i; j--)
        {
            if (((IComparable)arrayToSort[j - 1]).CompareTo(arrayToSort[j]) > 0)
            {
                object temp = arrayToSort[j - 1];
                arrayToSort[j - 1] = arrayToSort[j];
                arrayToSort[j] = temp;
                RedrawItem(j);
                RedrawItem(j - 1);
                pnlSamples.Refresh();
                if (chkCreateAnimation.Checked)
                    SavePicture();
            }
        }
    }
    return arrayToSort;
}
Worst case performance: O(n2)
Best case performance: O(n)
Average case performance: O(n2)
Worst case space complexity: O(1) auxiliary

双向冒泡排序

鸡尾酒排序,也称为双向冒泡排序、调酒器排序、搅拌排序(可以参考选择排序的一个变种)、纹波排序、接送排序,或欢乐时光排序。它由冒泡排序变化而来,是一种稳定的比较排序算法。该算法不同于冒泡排序,它在排序上由两个方向同时进行。该排序算法只是比冒泡排序稍难实现,但解决了冒泡排序中的“乌龟问题”(数组尾部的小值)。

首先从左向右方向为大元素移动方向,从右向左方向为小元素移动方向,然后每个元素都依次执行。在第 i 次移动后,前 i 个元素和后个 i 元素都放到了正确的位置,也不需要在检查一次。每次缩短已排序的那部分列表,都可以减半操作次数。

但是在乱数序列的状态下,双向冒泡排序与冒泡排序的效率都很差劲。

public IList BiDerectionalBubleSort(IList arrayToSort) 
{
    int limit = arrayToSort.Count;
    int st = -1;
    bool swapped = false;
    do
    {
        swapped = false;
        st++;
        limit--;

        for (int j = st; j < limit; j++)
        {
            if (((IComparable)arrayToSort[j]).CompareTo(arrayToSort[j + 1]) > 0)
            {
                object temp = arrayToSort[j];
                arrayToSort[j] = arrayToSort[j + 1];
                arrayToSort[j + 1] = temp;
                swapped = true;
                RedrawItem(j);
                RedrawItem(j + 1);
                pnlSamples.Refresh();
                if(chkCreateAnimation.Checked)
                    SavePicture();

            }
        }
        for (int j = limit - 1; j >= st; j--)
        {
            if (((IComparable)arrayToSort[j]).CompareTo(arrayToSort[j + 1]) > 0)
            {
                object temp = arrayToSort[j];
                arrayToSort[j] = arrayToSort[j + 1];
                arrayToSort[j + 1] = temp;
                swapped = true;
                RedrawItem(j);
                RedrawItem(j + 1);

                pnlSamples.Refresh();
                if (chkCreateAnimation.Checked)
                    SavePicture();

            }
        }

    } while (st < limit && swapped);

    return arrayToSort;
}
Worst case performance: O(n2)
Best case performance: O(n)
Average case performance: O(n2)
Worst case space complexity: O(1) auxiliary

桶排序

桶排序,或称为箱排序,是一种把数列划分成若干个桶的排序算法。在每个桶内各自排序,或者使用不同的排序算法,或通过递归方式继续使用桶排序算法。这是一个分布排序,是最能体现出数字意义的一种基数排序。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。

桶排序的流程:

  1. 设置一个定量的数组当作空桶子。
  2. 寻访串行,并且把项目一个一个放到对应的桶子去。
  3. 对每个不是空的桶子进行排序。
  4. 从不是空的桶子里把项目再放回原来的串行中。
public IList BucketSort(IList arrayToSort)
{
    if (arrayToSort == null || arrayToSort.Count == 0) return arrayToSort;

    object max = arrayToSort[0];
    object min = arrayToSort[0];

    for (int i = 0; i  0)
        {
            max = arrayToSort[i];
        }

        if (((IComparable)arrayToSort[i]).CompareTo(min) < 0)
        {
            min = arrayToSort[i];
        }
    }
    ArrayList[] holder = new ArrayList[(int)max - (int)min + 1];

    for (int i = 0; i < holder.Length; i++)
    {
        holder[i] = new ArrayList();
    }

    for (int i = 0; i < arrayToSort.Count; i++)
    {
        holder[(int)arrayToSort[i] - (int)min].Add(arrayToSort[i]);
    }

    int k = 0;

    for (int i = 0; i  0)
        {
            for (int j = 0; j < holder[i].Count; j++)
            {
                arrayToSort[k] = holder[i][j];
                RedrawItem(k);
                pnlSamples.Refresh();
                if (chkCreateAnimation.Checked)
                    SavePicture();
                k++;
            }
        }
    }

    return arrayToSort;
}
Worst case performance: O(n2.k)
Best case performance: -
Average case performance: O(n.k)
Worst case space complexity: O(n.k)

梳排序

梳排序是一个相对简单的排序算法,最初它由Wlodzimierz Dobosiewicz于1980年设计出来。后来由斯蒂芬和理查德重新发现和推广。他们的文章在1991年4月发表在字节杂志上。梳排序改善了冒泡排序和类似快速排序的竞争算法。其要旨在于消除乌龟,亦即在阵列尾部的小数值,这些数值是造成冒泡排序缓慢的主因。相对地,兔子,亦即在阵列前端的大数值,不影响冒泡排序的效能。

在冒泡排序中,任何两个元素进行比较时,他们总是距离彼此为1。梳排序的基本思想是可以不是1。

梳排序中,开始时的间距设定为列表长度,然后每一次都会除以损耗因子(一般为1.3)。必要的时候,间距可以四舍五入,不断重复,直到间距变为1。然后间距就保持为1,并排完整个数列。最后阶段就相当于一个冒泡排序,但此时大多数乌龟已经处理掉,此时的冒泡排序就高效了。

public IList CombSort(IList arrayToSort)
{
    int gap = arrayToSort.Count;
    int swaps = 0;

    do
    {
        gap = (int)(gap / 1.247330950103979);
        if (gap  0)
            {
                object temp = arrayToSort[i];
                arrayToSort[i] = arrayToSort[i + gap];
                arrayToSort[i + gap] = temp;
                RedrawItem(i);
                RedrawItem(i + gap);
                pnlSamples.Refresh();
                if (chkCreateAnimation.Checked)
                    SavePicture();
                swaps = 1;
            }
            i++;
        } while (!(i + gap >= arrayToSort.Count));

    } while (!(gap == 1 && swaps == 0));

    return arrayToSort;
}
Worst case performance: -
Best case performance: -
Average case performance: -
Worst case space complexity: O(1)

圈排序

Cycle sort is an in-place, unstable sorting algorithm, a comparison sort that is theoretically optimal in terms of the total number of writes to the original array, unlike any other in-place sorting algorithm. It is based on the idea that the permutation to be sorted can be factored into cycles, which can individually be rotated to give a sorted result.

Unlike nearly every other sort, items are never written elsewhere in the array simply to push them out of the way of the action. Each value is either written zero times, if it’s already in its correct position, or written one time to its correct position. This matches the minimal number of overwrites required for a completed in-place sort.

它是一个就地、不稳定的排序算法,根据原始的数组,一种理论上最优的比较,并且与其它就地排序算法不同。它的思想是把要排的数列分解为圈,即可以分别旋转得到排序结果。

不同于其它排序的是,元素不会被放入数组的中任意位置从而推动排序。每个值如果它已经在其正确的位置则不动,否则只需要写一次即可。也就是说仅仅最小覆盖就能完成排序。

public IList CycleSort(IList arrayToSort)
{
    int writes = 0;
    for (int cycleStart = 0; cycleStart < arrayToSort.Count; cycleStart++)
    {
        object item = arrayToSort[cycleStart];
        int pos = cycleStart;

        do
        {
            int to = 0;
            for (int i = 0; i < arrayToSort.Count; i++)
            {
                if (i != cycleStart && ((IComparable)arrayToSort[i]).CompareTo(item) < 0)
                {
                    to++;
                }
            }
            if (pos != to)
            {
                while (pos != to && ((IComparable)item).CompareTo(arrayToSort[to]) == 0)
                {
                    to++;
                }

                object temp = arrayToSort[to];
                arrayToSort[to] = item;
                RedrawItem(to);
                item = temp;
                RedrawItem(cycleStart);
                pnlSamples.Refresh();
                if (chkCreateAnimation.Checked)
                    SavePicture();
                writes++;
                pos = to;
            }
        } while (cycleStart != pos);
    }
    return arrayToSort;
}
Worst case performance: O(n2)
Best case performance: -
Average case performance: O(n2)
Worst case space complexity: O(1)

地精排序

地精排序(gnome sorting,大部分地方翻成“侏儒排序”,“地精排序”明明更好听呀,本文就这么用了。)最初由哈米德在2000年的时候提出,当时称为傻瓜排序,之后被迪克说明并且命名为“地精排序”。除了某个元素是经过一系列的互换(类似冒泡排序)才到了它的正确位置之外,它和插入排序挺相似。

它在概念上很简单,不需要嵌套循环。运行时间是O(n2),如果列表基本有序,则时间为O(n)。实际上,它和插入排序一样,平均运行时O(n2)。

The algorithm always finds the first place where two adjacent elements are in the wrong order, and swaps them. It takes advantage of the fact that performing a swap can introduce a new out-of-order adjacent pair only right before or after the two swapped elements. It does not assume that elements forward of the current position are sorted, so it only needs to check the position directly before the swapped elements.

地精算法总是发现第一个 【两个相邻元素存在错误的顺序】,然后把他们交换。原理是,交换一对乱序元素后,会产生一对新的无序相邻元素,而这两个元素要么交换前有序,要么交换后有序。它不认为元素当前的位置是有序的,所以它只需要在交换元素前直接检查位置。

public IList GnomeSort(IList arrayToSort)
{
    int pos = 1;
    while (pos = 0)
        {
            pos++;
        }
        else
        {
            object temp = arrayToSort[pos];
            arrayToSort[pos] = arrayToSort[pos - 1];
            RedrawItem(pos);

            arrayToSort[pos - 1] = temp;
            RedrawItem(pos - 1);
            RefreshPanel(pnlSamples);
            if (savePicture)
                SavePicture();
            if (pos > 1)
            {
                pos--;
            }
        }
    }
    return arrayToSort;
}
Worst case performance: O(n2)
Best case performance: -
Average case performance: -
Worst case space complexity: O(1)

堆排序

堆排序是从数据集构建一个数据堆,然后提取最大元素,把它放到部分有序的数组的末尾。提取最大元素后,重新构建新堆,然后又接着提取新堆这的最大元素。重复这个过程,直到堆中没有元素并且数组已排好。堆排序的基本实现需要两个数组:一个用于构建堆,另一个是存放有序的元素。

堆排序把输入数组插入到一个二叉堆的数据结构中。最大值(大根堆)或最小值(小根堆)会被提取出来,直到堆空为止,提取出来的元素,是已经排好序的。每次提取后,堆中没变换的元素依然保留了,所以(堆排序的)唯一消耗就是提取过程。

在提取过程中,所需要的空间,就是用于存放堆的空间。为了实现恒定的空间开销,堆是存储在输入数组中还没有完成排序的那部分空间中。堆排序使用了两个堆操作:插入和根删除。每个提取的元素放到数组的最后一个空位置。数组剩余位置存放待排元素。

public IList HeapSort(IList list)
{
    for (int i = (list.Count - 1) / 2; i >= 0; i--)
        Adjust(list, i, list.Count - 1);

    for (int i = list.Count - 1; i >= 1; i--)
    {
        object Temp = list[0];
        list[0] = list[i];
        list[i] = Temp;
        RedrawItem(0);
        RedrawItem(i);
        pnlSamples.Refresh();
        if (chkCreateAnimation.Checked)
            SavePicture();
        Adjust(list, 0, i - 1);

    }

    return list;
}

public void Adjust(IList list, int i, int m)
{
    object Temp = list[i];
    int j = i * 2 + 1;
    while (j <= m)
    {
        if (j < m)
            if (((IComparable)list[j]).CompareTo(list[j + 1]) < 0)
                j = j + 1;

        if (((IComparable)Temp).CompareTo(list[j]) < 0)
        {
            list[i] = list[j];
            RedrawItem(i);
            pnlSamples.Refresh();
            if (chkCreateAnimation.Checked)
                SavePicture();
            i = j;
            j = 2 * i + 1;
        }
        else
        {
            j = m + 1;
        }
    }
    list[i] = Temp;
    RedrawItem(i);
    pnlSamples.Refresh();
    if (chkCreateAnimation.Checked)
        SavePicture();
}
Worst case performance: O(n log n)
Best case performance: O(n log n)
Average case performance: O(n log n)
Worst case space complexity: O(1)

插入排序

插入排序的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

具体算法描述如下:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5
public IList InsertionSort(IList arrayToSort)
{
    for (int i = 1; i < arrayToSort.Count; i++)
    {
        object val = arrayToSort[i];
        int j = i - 1;
        bool done = false;
        do
        {
            if (((IComparable)arrayToSort[j]).CompareTo(val) > 0)
            {
                arrayToSort[j + 1] = arrayToSort[j];
                RedrawItem(j + 1);
                pnlSamples.Refresh();
                if (chkCreateAnimation.Checked)
                    SavePicture();
                j--;
                if (j < 0)
                {
                    done = true;
                }
            }
            else
            {
                done = true;
            }

        } while (!done);
        arrayToSort[j + 1] = val;

        RedrawItem(j + 1);
        pnlSamples.Refresh();
        if (chkCreateAnimation.Checked)
            SavePicture();
    }
    return arrayToSort;
}
Worst case performance: O(n2)
Best case performance: O(n)
Average case performance: O(n2)
Worst case space complexity: O(1)

归并排序

从概念上讲,归并排序的工作原理如下:

  • 如果列表的长度是0或1,那么它已经有序。否则:
  • 未排序的部分平均划分为两个子序列。
  • 每个子序列,递归使用归并排序。
  • 合并两个子列表,使之整体有序。

归并排序包含两个主要观点,以改善其运行时:

  • 一个小列表排序的花费少于大列表。
  • 把两个有序表合并,要比直接排列一个无序表花费更少的步骤。例如,您只需要遍历每个有序列表一次即可(见下面的合并功能)。

归并操作的过程如下:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾
public IList MergeSort(IList a, int low, int height)
{
    int l = low;
    int h = height;

    if (l >= h)
    {
        return a;
    }

    int mid = (l + h) / 2;

    MergeSort(a, l, mid);
    MergeSort(a, mid + 1, h);

    int end_lo = mid;
    int start_hi = mid + 1;
    while ((l <= end_lo) && (start_hi <= h))
    {
        if (((IComparable)a[l]).CompareTo(a[start_hi]) < 0)
        {
            l++;
        }
        else
        {
            object temp = a[start_hi];
            for (int k = start_hi - 1; k >= l; k--)
            {
                a[k + 1] = a[k];
                RedrawItem(k + 1);
                pnlSamples.Refresh();
                if (chkCreateAnimation.Checked)
                    SavePicture();
            }
            a[l] = temp;
            RedrawItem(l);
            pnlSamples.Refresh();
            if (chkCreateAnimation.Checked)
                SavePicture();
            l++;
            end_lo++;
            start_hi++;
        }
    }
    return a;
}
Worst case performance: O(n log n)
Best case performance: O(n log n)
Average case performance: O(n log n)
Worst case space complexity:

奇偶排序

Odd-even sort is a relatively simple sorting algorithm. It is a comparison sort based on bubble sort with which it shares many characteristics. It functions by comparing all (odd, even)-indexed pairs of adjacent elements in the list and, if a pair is in the wrong order (the first is larger than the second) the elements are switched. The next step repeats this for (even, odd)-indexed pairs (of adjacent elements). Then it alternates between (odd, even) and (even, odd) steps until the list is sorted. It can be thought of as using parallel processors, each using bubble sort but starting at different points in the list (all odd indices for the first step). This sorting algorithm is only marginally more difficult than bubble sort to implement.

奇偶排序是一个相对简单的排序算法。它是一种基于冒泡排序的比较算法,它们有着许多共同点。它通过比较所有相邻的(奇数偶)对进行排序,如果某对存在错误的顺序(第一个元素大于第二个),则交换。下一步针对{偶奇对}重复这一操作。然后序列就在(奇,偶)和(偶,奇)之间变换,直到列表有序。它可以看作是是使用并行处理器,每个都用了冒泡排序,但只是起始点在列表的不同位置(所有奇数位置可做第一步)。这个排序算法实现起来只是略微比冒泡排序复杂一些。

public IList OddEvenSort(IList arrayToSort)
{
    bool sorted = false;
    while (!sorted)
    {
        sorted = true;
        for (var i = 1; i < arrayToSort.Count - 1; i += 2)
        {
            if (((IComparable)arrayToSort[i]).CompareTo(arrayToSort[i + 1]) > 0)
            {
                object temp = arrayToSort[i];
                arrayToSort[i] = arrayToSort[i + 1];
                RedrawItem(i);
                arrayToSort[i + 1] = temp;
                RedrawItem(i+1);
                pnlSamples.Refresh();
                if (chkCreateAnimation.Checked)
                    SavePicture();
                sorted = false;
            }
        }

        for (var i = 0; i < arrayToSort.Count - 1; i += 2)
        {
            if (((IComparable)arrayToSort[i]).CompareTo(arrayToSort[i + 1]) > 0)
            {
                object temp = arrayToSort[i];
                arrayToSort[i] = arrayToSort[i + 1];
                arrayToSort[i + 1] = temp;
                RedrawItem(i);
                RedrawItem(i+1);
                pnlSamples.Refresh();
                if (chkCreateAnimation.Checked)
                    SavePicture();
                sorted = false;
            }
        }
    }
    return arrayToSort;
}

鸽巢排序

鸽巢排序,也被称为 count sort (不要和 counting sort 搞混了),当数组元素的元素数量(n)和可能的键值数(key value,N)大约相同时,这种排序算法实用。它的时间复杂度为O(n+N)。

(在不可避免遍历每一个元素并且排序的情况下效率最好的一种排序算法。但它只有在差值(或者可被映射在差值)很小的范围内的数值排序的情况下实用。)

算法流程如下:

  • Given an array of values to be sorted, set up an auxiliary array of initially empty “pigeonholes”, one pigeonhole for each key through the range of the original array. 假设有个一个待排序的数组,给它建立了一个空的辅助数组(称为“鸽巢”)。把原始数组中的每个值作为一个key(“格子”)。
  • Going over the original array, put each value into the pigeonhole corresponding to its key, such that each pigeonhole eventually contains a list of all values with that key. 遍历原始数组,根据每个值放入辅助数组对应的“格子”
  • Iterate over the pigeonhole array in order, and put elements from non-empty pigeonholes back into the original array. 顺序遍历“鸽巢”数组(辅助数组),把非空鸽巢中的元素放回原始数组。
public IList PigeonHoleSort(IList list)
{
    object min = list[0], max = list[0];
    foreach (object x in list)
    {
        if (((IComparable)min).CompareTo(x) > 0)
        {
            min = x;
        }
        if (((IComparable)max).CompareTo(x) < 0)
        {
            max = x;
        }
        Thread.Sleep(speed);
    }

    int size = (int)max - (int)min + 1;

    int[] holes = new int[size];

    foreach (int x in list)
        holes[x - (int)min]++;

    int i = 0;
    for (int count = 0; count < size; count++)
        while (holes[count]-- > 0)
        {

            list[i] = count + (int)min;
            RedrawItem(i);
            i++;
            RefreshPanel(pnlSamples);
            Thread.Sleep(speed);
        }
    return list;
}
Worst case performance: O(n+2k)
Best case performance: -
Average case performance: O(n+2k)
Worst case space complexity: O(2k)

快速排序

快速排序采用分而治之的策略,把一个列表划分为两个子列表。步骤是:

  • 从列表中,选择一个元素,称为基准(pivot)。
  • 重新排序列表,把所有数值小于枢轴的元素排到基准之前,所有数值大于基准的排基准之后(相等的值可以有较多的选择)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  • 分别递归排序较大元素的子列表和较小的元素的子列表。

递归的结束条件是列表元素为零或一个,即已不需要排序。

public IList QuickSort(IList a, int left, int right)
{
    int i = left;
    int j = right;
    double pivotValue = ((left + right) / 2);
    int x = (int)a[int.Parse(pivotValue.ToString())];

    while (i <= j)
    {
        while (((IComparable)a[i]).CompareTo(x) < 0)
        {
            i++;
        }
        while (((IComparable)x).CompareTo(a[j]) < 0)
        {
            j--;
        }
        if (i <= j)
        {
            object temp = a[i];
            a[i] = a[j];
            RedrawItem(i);
            i++;
            a[j] = temp;
            RedrawItem(j);
            j--;
            pnlSamples.Refresh();
            if (chkCreateAnimation.Checked)
                SavePicture();
        }
    }
    if (left < j)
    {
        QuickSort(a, left, j);
    }
    if (i < right)
    {
        QuickSort(a, i, right);
    }
    return a;
}
Worst case performance: O(n2)
Best case performance: O(n log n)
Average case performance: O(n log n)
Worst case space complexity: O(log n)

使用冒泡的快速排序

public IList BubbleSort(IList arrayToSort, int left, int right)
{
    for (int i = left; i < right; i++)
    {
        for (int j = right; j > i; j--)
        {
            if (((IComparable)arrayToSort[j - 1]).CompareTo(arrayToSort[j]) > 0)
            {
                object temp = arrayToSort[j - 1];
                arrayToSort[j - 1] = arrayToSort[j];
                RedrawItem(j-1);
                arrayToSort[j] = temp;
                RedrawItem(j);
                pnlSamples.Refresh();
                if (chkCreateAnimation.Checked)
                    SavePicture();
            }
        }
    }

    return arrayToSort;
}

public IList QuickSortWithBubbleSort(IList a, int left, int right)
{
    int i = left;
    int j = right;

    if (right - left <= 6)
    {
        BubbleSort(a, left, right);
        return a;
    }

    double pivotValue = ((left + right) / 2);
    int x = (int)a[int.Parse(pivotValue.ToString())];

    a[(left + right) / 2] = a[right];
    a[right] = x;
    RedrawItem(right);
    pnlSamples.Refresh();
    if (chkCreateAnimation.Checked)
        SavePicture();

    while (i <= j)
    {
        while (((IComparable)a[i]).CompareTo(x) < 0)
        {
            i++;
        }
        while (((IComparable)x).CompareTo(a[j]) < 0)
        {
            j--;
        }

        if (i <= j)
        {
            object temp = a[i];
            a[i++] = a[j];
            RedrawItem(i - 1);
            a[j--] = temp;
            RedrawItem(j + 1);
            pnlSamples.Refresh();
            if (chkCreateAnimation.Checked)
                SavePicture();
        }
    }
    if (left < j)
    {
        QuickSortWithBubbleSort(a, left, j);
    }
    if (i < right)
    {
        QuickSortWithBubbleSort(a, i, right);
    }

    return a;
}

选择排序

原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

算法过程如下:

  • 找到列表中的最小值,
  • 把它和第一个位置的元素交换,
  • 列表其余部分重复上面的步骤(从第二个位置开始,且每次加1).

列表被有效地分为两个部分:从左到右的有序部分,和余下待排序部分。

public IList SelectionSort(IList arrayToSort)
{
    int min;
    for (int i = 0; i < arrayToSort.Count; i++)
    {
        min = i;
        for (int j = i + 1; j < arrayToSort.Count; j++)
        {
            if (((IComparable)arrayToSort[j]).CompareTo(arrayToSort[min]) < 0)
            {
                min = j;
            }
        }
        object temp = arrayToSort[i];
        arrayToSort[i] = arrayToSort[min];
        arrayToSort[min] = temp;

        RedrawItem(i);
        RedrawItem(min);
        pnlSamples.Refresh();
        if (chkCreateAnimation.Checked)
            SavePicture();    
    }

    return arrayToSort;
}
Worst case performance: O(n2)
Best case performance: O(n2)
Average case performance: O(n2)
Worst case space complexity: O(1)

希尔排序

希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。

假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O(n2)的排序(冒泡排序或插入排序),可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。

一个更好理解的希尔排序实现:将数组列在一个表中并对列排序(用插入排序)。重复这过程,不过每次用更长的列来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身仅仅对原数组进行排序(通过增加索引的步长,例如是用i += step_size而不是i++)。

例如,假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样:

13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10

然后我们对每列进行排序:

10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45

将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].这时10已经移至正确位置了,然后再以3为步长进行排序:

10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45

排序之后变为:

10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94

最后以1步长进行排序(此时就是简单的插入排序了)。

public IList ShellSort(IList arrayToSort)
{
    int i, j, increment;
    object temp;

    increment = arrayToSort.Count / 2;

    while (increment > 0)
    {
        for (i = 0; i < arrayToSort.Count; i++)
        {
            j = i;
            temp = arrayToSort[i];
            while ((j >= increment) && 
                  (((IComparable)arrayToSort[j - increment]).CompareTo(temp) > 0))
            {
                arrayToSort[j] = arrayToSort[j - increment];
                RedrawItem(j);
                pnlSamples.Refresh();
                if (chkCreateAnimation.Checked)
                    SavePicture();
                j = j - increment;
            }
            arrayToSort[j] = temp;
            RedrawItem(j);
            pnlSamples.Refresh();
            if (chkCreateAnimation.Checked)
                SavePicture();
        }
        if (increment == 2)
            increment = 1;
        else
            increment = increment * 5 / 11;
    }

    return arrayToSort;
}
Worst case performance: -
Best case performance: n
Average case performance: O(n log2 n)
Worst case space complexity: O(1)

最后,感谢给与我帮助的人,有了你们的帮助,本文的质量有了更大的提高,谢谢!

可视化对比十多种排序算法(C#版),首发于博客 - 伯乐在线

25 May 10:33

知无涯之std::sort源码剖析

by feihu

从事程序设计行业的朋友一定对排序不陌生,它从我们刚刚接触数据结构课程开始便伴随我们左右,是需要掌握的重要技能。任何一本数据结构的教科书一定会介绍各种各样的排序算法,比如最简单的冒泡排序、插入排序、希尔排序、堆排序等。在现已知的所有排序算法之中,快速排序名如其名,以快速著称,它的平均时间复杂度可以达到O(N logN),是最快排序算法之一。

目录

背景

在校期间,为了掌握这些排序算法,我们不得不经常手动实现它们,以加深对其的理解。然而这些算法实在是太常用了,我们不太可能在每次需要时都手动来实现,不管是性能还是安全性都得不到保证。因此这些算法被包含进了很多语言的标准库里,在C语言的标准库中,stdlib.h头文件就有qsort算法,它正是最快排序算法——快速排序的标准实现,这给我们提供了很大的方便。

然而,快速排序虽然平均复杂度为O(N logN),却可能由于不当的pivot选择,导致其在最坏情况下复杂度恶化为O(N2)。另外,由于快速排序一般是用递归实现,我们知道递归是一种函数调用,它会有一些额外的开销,比如返回指针、参数压栈、出栈等,在分段很小的情况下,过度的递归会带来过大的额外负荷,从而拉缓排序的速度。

Introspective Sort

为了解决快速排序在最坏情况下复杂度恶化的问题,人们进行了大量的研究,获得了众多研究成果。本文将要介绍的算法便是其中之一。在开始之前我们需要先简短介绍两个其它常用的算法,这对我们理解新算法为何如此设计非常重要,它们是堆排序和插入排序。

堆排序的优点

堆排序经常是作为快速排序最有力的竞争者出现,它们的复杂度都是O(N logN)。这里有一个维基百科上的动态图片,直观的反应出堆排序的过程:

堆排序

虽然两者拥有一样的复杂度,但就平均表现而言,它却比快速排序慢了2~5倍,知乎上有一个讨论:堆排序缺点何在?另外还可以参考Comparing Quick and Heap Sorts,还有Basic Comparison of Heap-Sort and Quick-Sort Algorithms,这些都给出了为何堆排序相比快速排序而言慢了这许多。

但是,有一点它却比快速排序要好很多:最坏情况它的复杂度仍然会保持O(N logN),这一优点对本文介绍的新算法有着巨大的作用。

插入排序的优点

再来看看插入排序,同样有一张维基百科上的动态图片,可以唤起你对它的记忆:

插入排序

它在数据大致有序的情况表现非常好,可以达到O(N),可以参考这个讨论Which sort algorithm works best on mostly sorted data?这一优点也被新算法所采用。

横空出世

到了正式介绍新算法的时刻。由于快速排序有着前面所描述的问题,因此Musser在1996年发表了一遍论文,提出了Introspective Sorting(内省式排序),这里可以找到PDF版本。它是一种混合式的排序算法,集成了前面提到的三种算法各自的优点:

  • 在数据量很大时采用正常的快速排序,此时效率为O(logN)。
  • 一旦分段后的数据量小于某个阈值,就改用插入排序,因为此时这个分段是基本有序的,这时效率可达O(N)。
  • 在递归过程中,如果递归层次过深,分割行为有恶化倾向时,它能够自动侦测出来,使用堆排序来处理,在此情况下,使其效率维持在堆排序的O(N logN),但这又比一开始使用堆排序好。

由此可知,它乃综合各家之长的算法。也正因为如此,C++的标准库就用其作为std::sort的标准实现。

std::sort的实现

SGI版本的STL一直是评价最高的一个STL实现,在技术层次、源代码组织、源代码可读性上,均有卓越表现。所以它被纳为GNU C++标准程序库。这里选择了侯捷的《STL源码剖析》一书中分析的GNU C++ 2.91版本来作分析,此版本稳定且可读性强。

std::sort的代码如下:

1     template <class RandomAccessIterator>
2     inline void sort(RandomAccessIterator first, RandomAccessIterator last) {
3         if (first != last) {
4             __introsort_loop(first, last, value_type(first), __lg(last - first) * 2);
5             __final_insertion_sort(first, last);
6         }
7     }

它是一个模板函数,只接受随机访问迭代器。if语句先判断区间有效性,接着调用__introsort_loop,它就是STL的Introspective Sort实现。在该函数结束之后,最后调用插入排序。我们来揭开该算法的面纱:

 1     template <class RandomAccessIterator, class T, class Size>
 2     void __introsort_loop(RandomAccessIterator first,
 3                           RandomAccessIterator last, T*,
 4                           Size depth_limit) {
 5         while (last - first > __stl_threshold) {
 6             if (depth_limit == 0) {
 7                 partial_sort(first, last, last);
 8                 return;
 9             }
10             --depth_limit;
11             RandomAccessIterator cut = __unguarded_partition
12               (first, last, T(__median(*first, *(first + (last - first)/2),
13                                        *(last - 1))));
14             __introsort_loop(cut, last, value_type(first), depth_limit);
15             last = cut;
16         }
17     }

这是算法主体部分,代码虽然不长,但充满技巧,有很多细节需要注意,接下来我们将对其一一展开分析。

递归结构

可以看出它是一个递归函数,因为我们说过,Introspective Sort在数据量很大的时候采用的是正常的快速排序,因此除了处理恶化情况以外,它的结构应该和快速排序一致。但仔细看以上代码,先不管循环条件和if语句(它们便是处理恶化情况所用),循环的后半部分是用来递归调用快速排序。但它与我们平常写的快速排序有一些不同,对比来看,以下是我们平常所写的快速排序的伪代码

 1 function quicksort(array, left, right)
 2     // If the list has 2 or more items
 3     if left < right
 4         // See "#Choice of pivot" section below for possible choices
 5         choose any pivotIndex such that left ≤ pivotIndex ≤ right
 6         // Get lists of bigger and smaller items and final position of pivot
 7         pivotNewIndex := partition(array, left, right, pivotIndex)
 8         // Recursively sort elements smaller than the pivot (assume pivotNewIndex - 1 does not underflow)
 9         quicksort(array, left, pivotNewIndex - 1)
10         // Recursively sort elements at least as big as the pivot (assume pivotNewIndex + 1 does not overflow)
11         quicksort(array, pivotNewIndex + 1, right)

__introsort_loop中只有对右边子序列进行递归调用是不是?左边的递归不见了。的确,这里的写法可读性相对来说比较差,但是仔细一分析发现是有它的道理的,它并不是没有管左子序列。注意看,在分割原始区域之后,对右子序列进行了递归,接下来的last = cut将终点位置调整到了分割点,那么此时的[first, last)区间就是左子序列了。又因为这是一个循环结构,那么在下一次的循环中,左子序列便得到了处理。只是并未以递归来调用。

我们来比较一下两者的区别,试想,如果一个序列只需要递归两次便可结束,即它可以分成四个子序列。原始的方式需要两个递归函数调用,接着两者各自调用一次,也就是说进行了7次函数调用,如下图左边所示。但是STL这种写法每次划分子序列之后仅对右子序列进行函数调用,左边子序列进行正常的循环调用,如下图右边所示。

两种递归调用对比

两者区别就在于STL节省了接近一半的函数调用,由于每次的函数调用有一定的开销,因此对于数据量非常庞大时,这一半的函数调用可能能够省下相当可观的时间。真是为了效率无所不用其极,令人惊叹!更关键是这并没有带来太多的可读性的降低,稍稍一经分析便能够读懂。这种稍稍以牺牲可读性来换取效率的做法在STL的实现中比比皆是,本文后面还会有例子。

三点中值法

先从这种惊叹中回过神来,接着看循环的主体部分,其中有一个__median函数,它的作用是取首部、尾部和中部三个元素的中值作为pivot。我们之前学到的快速排序都是选择首部、尾部或者中间位置的元素作为pivot,并不会比较它们的值,在很多情况下这将引起递归的恶化。现在这里采用的中值法可以在绝大部分情形下优于原来的选择。

分割算法

主循环中另外一个重要的函数是__unguarded_partition,这其实就是我们平常所使用的快速排序主体部分,用于根据pivot将区间分割为两个子序列。其源码如下:

 1     template <class RandomAccessIterator, class T>
 2     RandomAccessIterator __unguarded_partition(RandomAccessIterator first, 
 3                                                RandomAccessIterator last, 
 4                                                T pivot) {
 5         while (true) {
 6             while (*first < pivot) ++first;
 7             --last;
 8             while (pivot < *last) --last;
 9             if (!(first < last)) return first;
10             iter_swap(first, last);
11             ++first;
12         }
13     } 

它会不断去交换放错位置的元素,直到first和last指针相互交错为止,函数返回的是右边区间的起始位置。注意看:这个函数没有对first和last作边界检查,而是以两个指针交错作为中止条件,节约了比较运算的开支。可以这么做的理由是因为,选择是首尾中间位置三个值的中间值作为pivot,因此一定会在超出此有效区域之前中止指针的移动。《STL源码剖析》给出了两个非常直观的示意图:

分割示例一

分割示例一

分割示例二

分割示例二

相信这两个图可以让你非常容易明白这个分割算法。

递归深度阈值

现在我们来关注循环条件和if语句。__introsort_loop的最后一个参数depth_limit是前面所提到的判断分割行为是否有恶化倾向的阈值,即允许递归的深度,调用者传递的值为2logN。注意看if语句,当递归次数超过阈值时,函数调用partial_sort,它便是堆排序:

 1     template <class RandomAccessIterator, class T, class Compare>
 2     void __partial_sort(RandomAccessIterator first, RandomAccessIterator middle,
 3                         RandomAccessIterator last, T*, Compare comp) {
 4         make_heap(first, middle, comp);
 5         for (RandomAccessIterator i = middle; i < last; ++i)
 6             if (comp(*i, *first))
 7                 __pop_heap(first, middle, i, T(*i), comp, distance_type(first));
 8         sort_heap(first, middle, comp);
 9     }
10 
11     template <class RandomAccessIterator, class Compare>
12     inline void partial_sort(RandomAccessIterator first,
13                              RandomAccessIterator middle,
14                              RandomAccessIterator last, Compare comp) {
15         __partial_sort(first, middle, last, value_type(first), comp);
16     }

如前所述,此时采用堆排序可以将快速排序的效率从O(N2)提升到O(N logN),杜绝了过度递归所带来的开销。堆排序结束之后直接结束当前递归。

最小分段阈值

除了递归深度阈值以外,Introspective Sort还用到另外一个阈值。注意看__introsort_loop中的while语句,其中有一个变量__stl_threshold,其定义为:

1     const int __stl_threshold = 16;

它就是我们前面所说的最小分段阈值。当数据长度小于该阈值时,再使用递归来排序显然不划算,递归的开销相对来说太大。而此时整个区间内部有多个元素个数少于16的子序列,每个子序列都有相当程度的排序,但又尚未完全排序,过多的递归调用是不可取的。而这种情况刚好插入排序最拿手,它的效率能够达到O(N)。因此这里中止快速排序,sort会接着调用外部的__final_insertion_sort,即插入排序来处理未排序完全的子序列。

到目前为止一切都很好理解。

为何__final_insertion_sort如此实现

现在终于来到std::sort的最后一步——插入排序。将它作为单独的一章是因为它使用了些优化技巧,让人难以理解,我花了些时间才弄懂它,这也正是为何会有本文的根本原因。我们先来看看其定义:

 1     template <class RandomAccessIterator>
 2     void __final_insertion_sort(RandomAccessIterator first, 
 3                                 RandomAccessIterator last) {
 4         if (last - first > __stl_threshold) {
 5             __insertion_sort(first, first + __stl_threshold);
 6             __unguarded_insertion_sort(first + __stl_threshold, last);
 7         }
 8         else
 9             __insertion_sort(first, last);
10     }

它被分成了两个分支,前一个分支是处理大于分段阈值的情况,后一个分支处理小于等于分段阈值。第一个问题:为什么要划分成两种情况不同对待?

再看,第一个分支中又将区间分成了两段,前16个和剩余部分,然后分别调用两个排序。于是第二个问题来了,为什么要这么分段?

最后一个问题,__insertion_sort和__unguarded_insertion_sort有何区别?

这此问题便是我看到这个实现的疑惑,为什么不直接使用插入排序?但是很遗憾的是《STL源码剖析》并未讲得很清楚,网上也有类似的讨论,都说是为了优化,但为何这样便能优化,还是没有答案。如果你也无法回答上述三个问题,那么请跟随我一起来讨论。

各种插入排序算法的实现

我们这里先来看最后一个问题,这两种插入排序有何区别?要解释这个问题,需要先介绍它们各自的实现,从标准插入排序算法开始。

标准插入排序实现

插入排序很简单,本文前面的动态图可以很直观的展示它的原理。这里是摘自维基百科的一段伪代码:

1     for i ← 1 to length(A)
2         j ← i
3         while j > 0 and A[j-1] > A[j]
4             swap A[j] and A[j-1]
5             j ← j - 1

从第二个值开始遍历每个元素,首先判断是否有越界,然后判断是否需要交换。

__insertion_sort实现

那么同样都是插入排序,__insertion_sort和__unguarded_insertion_sort有何不同,为什么叫unguarded?接下来看看STL的实现:(注:这里取得都是采用默认比较函数的版本):

 1     template <class RandomAccessIterator, class T>
 2     void __unguarded_linear_insert(RandomAccessIterator last, T value) {
 3         RandomAccessIterator next = last;
 4         --next;
 5         while (value < *next) {
 6             *last = *next;
 7             last = next;
 8             --next;
 9         }
10         *last = value;
11     }
12 
13     template <class RandomAccessIterator, class T>
14     inline void __linear_insert(RandomAccessIterator first, 
15                                 RandomAccessIterator last, T*) {
16         T value = *last;
17         if (value < *first) {
18             copy_backward(first, last, last + 1);
19             *first = value;
20         }
21         else
22             __unguarded_linear_insert(last, value);
23     }
24 
25     template <class RandomAccessIterator>
26     void __insertion_sort(RandomAccessIterator first, RandomAccessIterator last) {
27         if (first == last) return; 
28         for (RandomAccessIterator i = first + 1; i != last; ++i)
29             __linear_insert(first, i, value_type(first));
30     }

最下面的函数,它是从第二个元素开始对每个元素依次调用了__linear_insert。后者和前面提到的标准插入排序有一点点不同,它会先将该值和第一个元素进行比较,如果比第一个元素还小,那么就直接将前面已经排列好的数据整体向后移动一位,然后将该元素放在起始位置。对于这种情况,和标准插入排序相比,它将last - first - 1次的比较与交换操作变成了一次copy_backward操作,节省了每次移动前的比较操作。

但这还不是最主要的。如果该元素并不小于第一个元素,它会调用另外一个函数__unguarded_linear_insert,这里仅仅挨个判断是否需要调换,找到位置之后就将其插入到适当位置。注意看,这里没有检查是否有越界,为什么可以这样?因为在__linear_insert的if语句中,已经可以确保第一个值在最左边了,如果不在最左边,它便不可能进入这个函数,会执行第一个分支。那么,__unguarded_linear_insert便可以毫无顾忌的省略掉越界的检查。当然,因为少了很多次的比较操作,效率肯定便有了提升。后面我们会就此作一个详细的分析。

注意:使用__unguarded_linear_insert时,一定得确保这个区间的左边有效范围内已经有了最小值,否则没有越界检查将可能带来非常严重的后果 。这种unguarded命名的函数在前面__introsort_loop里面也有一个:__unguarded_partition,这也是同样不考虑边界的情况,前面已经介绍过。

__unguarded_insertion_sort实现

最后再来看看__unguarded_insertion_sort在STL中的实现,同样这里只是默认比较函数版本:

 1     template <class RandomAccessIterator, class T>
 2     void __unguarded_insertion_sort_aux(RandomAccessIterator first, 
 3                                         RandomAccessIterator last, T*) {
 4         for (RandomAccessIterator i = first; i != last; ++i)
 5             __unguarded_linear_insert(i, T(*i));
 6     }
 7 
 8     template <class RandomAccessIterator>
 9     inline void __unguarded_insertion_sort(RandomAccessIterator first, 
10                                     RandomAccessIterator last) {
11         __unguarded_insertion_sort_aux(first, last, value_type(first));
12     }

可以忽略掉这层aux函数的包装,它只是为了获得迭代器所指向的类型,其实这两个函数可以合并为一个。这里直接对每个元素都调用__unguarded_linear_insert,这个函数我们在上节已经分析过,它不对边界作检查。正因为如此,它一定比前面的__insertion_sort要快。

但是有一点需要再次强调一遍:和前面的__unguarded_linear_insert一样,一定得确保这个区间的左边有效范围内已经有了最小值,否则没有越界检查将可能带来非常严重的后果

各种实现的性能分析

接下来我们对以上三种实现的性能作一个分析,这里仅以第i个元素的运算次数 作为比较,并不考虑编译器优化,所以只是一个粗略的性能分析。此时前i-1个元素已经排好,假设该第i个元素应该插入的位置离i的平均距离为N。

标准插入排序性能分析

对于标准插入排序,它需要的操作次数为:

1     // 标准插入排序伪代码
2     while j > 0 and A[j-1] > A[j]   // 2N次比较运算,N次减法运算
3         swap A[j] and A[j-1]        // N次交换运算(通常理解为3N次赋值运算)
4         j ← j - 1                   // N次自减运算

总共为2N次比较,3N次赋值,N次减法,N次自减。

__insertion_sort性能分析

再来看__insertion_sort,因为这里出现了分支,因此需要分开来对待。我们取两种极端情况,先假设每次都是取第一个分支,即value < *first,那么此时N=i:

1     // __linear_insert函数
2     if (value < *first) {           // 1次比较运算
3         copy_backward(first, last, last + 1);   
4                                     // 1次copy_backward
5         *first = value;             // 1次赋值运算
6     }

因为copy_backward最后调用的是memmove,它在C标准库中实现为:

1     // memmove函数
2     for (; 0 < n; --n)              // N次比较运算,N次自减运算
3         *sc1++ = *sc2++;            // 2N次自增运算,N次赋值运算

这里认为自增自减一样,因此总共需要N+1次比较,N+1次赋值,3N次自减。

如果假设每次__insertion_sort都不取第一个分支,即首位的元素已经是最小值,此时:

 1     // __linear_insert函数
 2     if (value < *first) {       // 1次比较
 3         // ...
 4     }
 5     else
 6         __unguarded_linear_insert(last, value);
 7                                 // 见下面
 8 
 9     // __unguarded_linear_insert函数
10     while (value < *next) {     // N次比较
11         *last = *next;          // N次赋值
12         last = next;            // N次赋值
13         --next;                 // N次自减
14     }

因此总共需要N+1次比较,2N次赋值和N次自减。

再假设两个分支有相同的概率(实际上第二个分支的可能性更大些,在经过__introsort_loop之后的数据更是如此,因为此时最小值已经可以确认位于前16个元素之后,这在后面有证明。因此很快最小值便可以移动到最左端,那么就必然走第二个分支),那么平均所需的操作为:N+0.5次比较,1.5N+0.5次赋值,2N次自减。

__unguarded_insertion_sort性能分析

由于其直接调用了__unguarded_linear_insert,因而和上述第二个分支类似,但没有了分支判断,所需操作为:N次比较,2N次赋值和N次自减。

比较结果

在上述基础上再作一点假设,假设每种运算占用CPU的时间一样,那么此时三种算法的结果分别为:

  • 2N + 3N + N + N = 7N
  • N + 0.5 + 1.5N + 0.5 + 2N = 4.5N + 1
  • N + 2N + N = 4N

假设总共为M个元素,那么平均执行次数为:

  • 7N * M
  • 4.5N * M + M
  • 4N * M

可以很明显的看到,后两种的执行次数远远低于第一种,接近标准实现的一半。而最后一种因为少了越界检查,乍看之下似乎无足轻重,但在M非常庞大的情况下,影响相当可观,毕竟这是一个非常根本的算法核心。这也是一直没有省略1的原因。

离真相更进一步

让我们回到__final_insertion_sort函数,为了唤醒你的记忆,再贴一次它的源代码:

 1     template <class RandomAccessIterator>
 2     void __final_insertion_sort(RandomAccessIterator first, 
 3                                 RandomAccessIterator last) {
 4         if (last - first > __stl_threshold) {
 5             __insertion_sort(first, first + __stl_threshold);
 6             __unguarded_insertion_sort(first + __stl_threshold, last);
 7         }
 8         else
 9             __insertion_sort(first, last);
10     }

此时前面提的最后一个问题:两种插入算法有何区别?已经有了答案:一个带边界检查而另一个不带,不带边界检查的__unguarded_insertion_sort更快。

那么为什么不直接使用它呢?还记得前面每次介绍它时都有一行加粗的话么?这是因为它有一个前提条件,那便是需要确保最小值已经存在于有效区间的最左边。于是,你可能会想,如果此时可以确定最小值已经位于最左边,那么后面所有的区间内便可以使用最快的__unguarded_insertion_sort算法。没错,STL的设计者也是这么想的。

可是,如何可以确定最小值已经在最左边了呢?或者在一个小的区间内?绝大部分情况下无法确定。但正是由于快速排序的特殊性,可以保证最小值存在于一个小的区域中,接下来我们会证明这一点。

所以他们想到将经过__introsort_loop排序的数据分成两段,假设第一段里面包含了最小值,那么将第一段使用__insertion_sort排序,后一段使用__unguarded_insertion_sort便可以达到效率的最大化。对,STL的设计者们珍爱效率如生命。

到这里,你可以回答第一个问题了:为什么有这样的分支处理?是因为如果数据量足够小,没有必要进行如此复杂的划分,直接一个插入排序便可以搞定。只数据量比较大的情况下,将数据分成两段,前一段使用带边界检查的插入排序,后一段使用不带边界检查的插入排序。

现在最为关键的一个问题来了,如何可以确保前16个元素中一定有最小值?

论证最小值存在于前16个元素之中

我们看一下维基百科上快速排序的动画,非常直观:

快速排序

从图中可以看出,无论经过几次递归调用,对于所有划分的区域,左边区间所有的数据一定比右边小,记住这一点,它将为后面的推理起到重要的作用。

再来看一眼__introsort_loop:

 1     template <class RandomAccessIterator, class T, class Size>
 2     void __introsort_loop(RandomAccessIterator first,
 3                           RandomAccessIterator last, T*,
 4                           Size depth_limit) {
 5         while (last - first > __stl_threshold) {
 6             if (depth_limit == 0) {
 7                 partial_sort(first, last, last);
 8                 return;
 9             }
10             --depth_limit;
11             RandomAccessIterator cut = __unguarded_partition
12               (first, last, T(__median(*first, *(first + (last - first)/2),
13                                        *(last - 1))));
14             __introsort_loop(cut, last, value_type(first), depth_limit);
15             last = cut;
16         }
17     }

该函数只有两种情况下可能返回,一是区域小于等于阈值16;二是超过递归深度阈值。我们现在只考虑最左边的子序列,先假设是由于第一种情况终止了这个函数,那么该子区域小于16。再根据前面的结论:左边区间的所有数据一定比右边小,可以推断出最小值一定在该小于16的子区域内。

假设函数是第二种情况下终止,那么对于最左边的区间,由于递归深度过深,因此该区间会调用堆排序,所以这段区间的最小值一定位于最左端。再加上前面的结论:左边区间所有的数据一定比右边小,那么该区间内最左边的数据一定是整个序列的最小值。

因此,不论是哪种情况,都可以保证起始的16个元素中一定有最小值。如此便能够使用__insertion_sort对前16个元素进行排序,接着用__unguarded_insertion_sort毫无顾忌的在不考虑边界的情况下对剩于的区间进行更快速的排序。

至此,所有三个问题都得到了解答。

std::sort适合哪些容器

这么高效的算法,是不是所有的容器都可以使用呢?我们常规数组是否也能使用?我们知道在STL中的容器可以大致分为:

  • 序列式容器:vector, list, deque
  • 关联式容器:set, map, multiset, multimap
  • 配置器容器:queue, stack, priority_queue
  • 无序关联式容器:unordered_set, unordered_map, unordered_multiset, unordered_multimap。这些是在C++ 11中引入的

对于所有的关联式容器如map和set,由于它们底层是用红黑树实现,因此已经具有了自动排序功能,不需要std::sort。至于配置器容器,因为它们对出口和入口做了限制,比如说先进先出,先进后出,因此它们也禁止使用排序功能。

由于std::sort算法内部需要去取中间位置元素的值,为了能够让访问元素更迅速,因此它只接受有随机访问迭代器的容器。对于所有的无序关联式容器而言,它们只有前向迭代器,因而无法调用std::sort。但我认为更为重要的是,从它们名称来看,本身就是无序的,它们底层是用哈希表来实现。它们的作用像是字典,为的是根据key快速访问对应的元素,所以对其排序是没有意义的。

剩下的三种序列式容器中,vector和deque拥有随机访问迭代器,因此它们可以使用std::sort排序。而list只有双向迭代器,所以它无法使用std::sort,但好在它提供了自己的sort成员函数

另外,我们最常使用的数组其实和vector一样,它的指针本质上就是一种迭代器,而且是随机访问迭代器,因此也可以使用std::sort。

写在最后

以上便是我所知道的std::sort的所有秘密。仅仅数十行代码,就包含了如此多的技巧,为得只有一个目的:尽最大可能提高算法效率。正如孟岩所说:

STL是精致的软件框架,是为优化效率而无所不用其极的艺术品,是数据结构与算法大师经年累月的智能结晶,是泛型思想的光辉诗篇,是C++高级技术的精彩亮相!

原来只是因为没看明白__final_insertion_sort函数,弄清之后才打算写一篇简短的文章来记录下,所以原来也打算只重点讨论这个函数。可写着写着就发现这个函数脱离不了std::sort,整个std::sort的实现过程中又有着各种各样的考虑,很多的细节在书中、网络上都没有得到解释,如果想要彻底弄明白它的话,需要花费些精力。就如侯捷在《STL源码剖析》一书的自序中所言,这本书的写作动机,纯属偶然。本文也一样,想到既然对这段代码花费了些时间去理解,并从中获益,那我想,应该会有其它人也能从这篇文章中获益吧,那为何不将关于这个算法的所有理解都整理出来呢?于是才有了本文最终的版本。写的过程中颇有感触,自我在脑海中或者笔记上总结,和汇聚成文相比完全是两回事,有太多的背景要介绍,述说的方式,结构的安排等等无一不需花费心思。现在可以体会出每个写作者的艰辛之处,更对认真完成众多经典作品的作者们充满了敬佩。

虽然现在人们认为STL存在非常多的诟病,比如引起代码膨胀、性能下降或者是编译信息难以阅读等,但我认为对于C++而言,它就像C的标准库相对于C语言,可以让我们的工作事半功倍,大大提高工作效率,它是语言不可或缺的部分。毕竟这是C++的标准委员会及众多C++专家们画了数十载的心血,无论是在稳定性、安全性、通用性还是效率上都经历住了很大的考验,如果自己从头开始设计一个自认为更好的库,真的能达到这么好的效果么?看看本文所讨论的std::sort,我实在很难想象还有比STL对它的实现更有效率的做法。当然,如果项目很特殊,比如禁用异常,或者已经有针对项目更高效稳定的量身定制的库,那么我可以理解禁止使用STL。但总的来说,在大部分情况下,有这么好的标准工具,为什么要熟视无睹呢?

(全文完)

feihu

2014.05.21 于 Shenzhen

08 May 00:25

Three Optimization Tips for C++

14 Dec 03:13

个人简历上用滥的词

by oioi

个人简历上用滥的词
职业社交网站LinkedIn 上有数百万的用户(2.59亿),他们都会用各种职场词语来描述自己。于是LinkedIn 公司闲着没事,列出来了:10大用得最多,但又没什么实际意义的词。来看看,这些词,你是不是也曾经用过。

2013年,10大滥词
1.responsible /有责任
2.strategic /有战略目光
3.creatvie /会创新
4.effective /有效率
5.patient /有耐心
6.expert /是专家
7.organizational /会组织
8.driven /会驱动
9.innovative /还是创新
10.analytical /还会分析

来看看2012年的,10大滥词(无排名)
responsible /有责任
effective /有效率
analytical /还会分析
creatvie /会创新
effective /有效率

experimental /实验?!
motivated /积极
multinational /在跨国公司
specialized /专业

根据这些词语 LinkedIn 也提醒大家:这是一个展示才华的平台,而不是要你放上这些虚浮的词语。比起这些没用的词,你的成果才是最重要的。

除此之外,这里还有一副地图,显示哪些国家,用哪些词比较多。以及,哪些词,只在哪些国家使用。

例如,Sustainable /可持续,只在荷兰有人用
Enthusiastic /热情,只有英国人在用
Passionate /热情,只有新西兰和澳大利亚人在用
Patient /耐心,只有美国人在用

个人简历上用滥的词

[oioi via dailymail]

>>点这里浏览原文

© 煎蛋 / 超载鸡微博 / 图片托管于又拍云存储 / 煎蛋7周年纪念款TEE


    


09 Dec 06:03

颠覆编程方式的感知编码:Stephen Wolfram雄心勃勃的全新计算模式

by tips+boxiyang@36kr.com(boxi)


2002年,出生在英国的科学家、程序员及创业家Stephen Wolfram的《一种新科学》刚刚发布,其颠覆传统的追求知识方式引发的惊愕、争议与指责就已经铺天盖地。上个月初,他在博客中披露了自己的一个即将完成的新项目,称该项目将会对技术世界乃至于技术以外的世界产生深远影响。

VB的John Koetsier在看了Wolfram的东西后说,那东西确实令人吃惊。无论你对他那本书的看法如何,有一件事情必须承认,他是个天才。


知识+计算=大事物

Wolfram的父母是二战前逃离德国到英国去的犹太人。他从小就显露出了过人之处。12岁时他已经撰写了一部物理词典,14岁时已经完成了3本粒子物理方面的书,15岁发表了第1篇科学论文。

1988年,他做出了科学计算平台Mathematica;2009年,他发布了计算知识搜索引擎Wolfram Alpha。而他的最新项目,则是这两者的完美联姻:

Mathematica是完美的精确计算引擎,WolframAlpha则是有关世界的一般信息。现在我们把二者结合到了一起。

这种结合只是大图景的一部分。新项目还包括了自然语言编程—这种自然语言并不是仅靠自然语言来完成编程,而是说开发者可以利用一部分的自然语言。此外应用中的一切会有一个新的定义,从代码到图像,从输入到结果,一切均可以符号表达式的方式使用和拓展。自动化也到了全新的水平,而且编程语言的开发跟以往完全不一样,抛弃了以往从小开始、以敏捷构造功能,建设库和模块为核心的做法,转为一种具备大规模整体性的东西—将数据和代码合二为一。还有就是对计算的全新专注,其对世界的了解甚至比程序员还要多。

野心比Google的知识图谱大多了

跟我们在Wolfram Alpha做的事情相比,知识图谱的心气就小得多了,那仅仅是维基百科和其他一些数据

Google希望理解对象和事物及其关系,以便给出回答而不仅仅是结果。但Wolfram希望让世界变成可计算的,这样的话计算机就可以回答诸如“现在国际空间站在哪里”之类的问题。这需要一定水平的机器智能,它得知道国际空间站是什么,还得知道它在太空中,知道它正在绕着地球轨道飞行,还得知道它的速度以及目前的轨道位置。

这不是静态的数据,而是计算与知识的结合。现在WolframAlpha做到了这一点,但这还仅仅是个开始。

Wolfram语言组件

Wolfram认为,搜索引擎不擅长这个东西,因为太凌乱了。搜索引擎中的问题会有很多答案,其适用性与正确性也各异。这没办法计算,不够简洁,无法进行编程或注入系统。

Wolfram说,让世界成为可计算的,这是一个比产生维基百科式信息要大得多的目标……一个迥然不同的东西。我们试图要做的远比这要更加雄心勃勃。

这件事情是如此的富有野心,意义是如此的深远,甚至到了难以描述的地步。Wolfram说在他这辈子做过的各种事情里面,这是最复杂的一个,复杂到可怕,难以解释。请记住,这是一个曾经写过粒子物理论文的人。这件事情需要渊深知识,牵涉面广,意义深远—Wolfram 称之为伸到编程、科学、知识及商业等不同领域的“触须”。

让计算机做这件事情

“总的说来,我们试图做的是,只要你能描述得出来想要什么,计算机就替你做。人来定义目标,然后计算机尽量去理解意思,然后尽最大努力去执行。”
Wolfram说。

他还进行了现场演示。

大约30秒钟,Wolfram就创建了一个小小的web应用,应用可以在网页上画圆,里面还包括有一个用户界面,通过它访客可以让圆圈变大变小或变颜色。编程如此简单要感谢Wolfram语言,由于它可以访问到浩瀚的知识库—所以知道什么是圆且可以画圆,它还可以自动提供web—原生的用户控制来操纵这个圆。这个例子只是个小意思,但过了30秒,Wolfram又写出了一个代码片段,代码实现了对南美国家的定义然后展示了相应国家的国旗。然后他调出一幅欧洲地图,通过计算的方式以不同的颜色高亮显示德国和法国,整个过程只需几秒钟


Wolfram语言解决“南美洲有哪些国?它们的国旗是什么?”这个问题

之所以能做出这样的东西,是因为新的Wolfram计算框架包括了Mathematica20年开发过程中形成的复杂而精确的算法,再加上WolframAlpha内部的知识引擎。结果是惊人的。

通过信息进行自动化

Wolfram 说这种自动化水平要比以往任何时候都要高,其强大令人难以置信,只要是WolframAlpha知道的,app都知道。

这是因为 Wolfram的自然语言处理技术。它知道南美洲是一个洲,因为知识引擎WolframAlpha知道这一点。同样地,它知道哪些国家属于南美洲,其国旗是什么,也了解相应国家的人口、地图形状及概况,也许还包括成千上万个其他的数据元素。而获取这一切只需输入“南美洲”即可。


1、2行代码即可完成一幅高亮显示德国和法国欧洲地图的调用。粗体的行是Wolfram自己输入的

换句话说,“南美洲”并不是一个被赋值的变量或待实例化的对象或类,而是一个机器知道和理解的短语,其含义、意思和关联均可毫不费力地植入程序中,且不需要外部的数据来源。而且该知识来源还会不断更新和发展来匹配不断更新和变化的世界。

这将是开发者开发应用的一大变化,而且这种编程方式不存在现实限制。

Wolfram进一步以南美洲作为类比,说正如我们了解厄瓜多尔的事情(如人口)一样,我们也可以了解Twitter API的东西。

由于具备快速创建应用的能力,Wolfram将成为游戏颠覆者。

自然语言输入—小孩也能写代码?

它改变了应用开发经济,因为以往需要数小时或数周完成的事情现在只需要几分钟。许多人都一些有趣的想法、算法或应用创意,但苦于缺人缺钱或缺时间而无法完成。Wolfram目前正在跟这些人会面,这一切将会改变。

Wolfram说自己的新项目将会催生一大批新的初创企业—在数小时内开发出一种算法或自动化系统将变成现实。

它还改变了程序员的范畴,因为代码将不再是动辄成千上万行,而是20到200行。这意味着娃娃也能写代码,菜鸟也能做出精彩的应用。


你想看图还是看代码?

Wolfram说,有了自然语言输入,谁将成为富有经验的程序员将会被改变。书写代码将被大大缩短—这是一门可以让你马上就能把事情干完的语言,不是那种“hello world”也要写上10行。而它将为书写复杂程序的人铺设好了坦途。

但这也会让你有点发懵。

感谢Mathematica的历史悠久以及WolframAlpha的大脑,Wolfram语言知道许多东西,也能通过内置函数对其进行操作—包括数据操纵和分析、可视化及制作图表,图像、地理、几何、声音、科学数据以及几乎自动化的用户界面开发,进入数据、社交数据,甚至在云端的部署。这是所有一切东西的大杂烩,甚至还要多得多,这正是它最晦涩难懂的地方—因为它跟传统的数据与代码及界面分离的做法实在是太不一样了。

当然,在具备自然语言输入的同时,Wolfram语言也有语法和结构以及操作符等,那些创建无缺陷的、可按你思路运行的程序所必须的构造物。这也意味着这门语言还是需要学习的—并非说谁都可以马上就能使唤它来开发应用。

Wolfram的用武之地:Raspberry Pi,智能手机、设备

这些应用有很多用武之地。

Wolfram最近发布了一个Raspberry Pi版的Mathematica。这不仅令人好奇:承载着浩大知识的Wolfram语言是如何被容纳进Pi小小的身躯内的?

奥秘在于它的引擎非常便携,但显然知识却是非常庞大的,所以Wolfram语言所需要的知识是集中存放到云端的,在处理时引擎会向云端索取知识。

Wolfram语言还可支持桌面应用、移动应用、web应用的开发,且既可支持公有云也可支持私有云。对于移动应用将会嵌入一个Wolfram引擎,然后通过API的方式去获取所需的数据。而所有代码均可复制粘贴于云端、设备及桌面之间。

不过不想学Wolfram语言也没问题,Wolfram说像Java那样的原生语言可以通过函数调用来利用Wolfram引擎。从表面上来看开发者仍只是在调用Java,但实际上后台会访问Wolfram的云。

感知编码,智能对象

由于Wolfram语言具备很高的自动化能力和智能水平,且对待数据和代码的方式十分类似,所以这种语言是不是可以被认为是具有感知能力的代码呢?

从某种程度来说是这样的。Wolfram解释说,他们试图做的,是让程序员设定目标,然后由计算机去琢磨如何实现目标。

但这并不是要让机器去创新手段,不过Wolfram也对让计算机去创新、创作感兴趣。比方说Wolfram Tones就是这样。这款音乐制作应用可以根据用户的输入自动创作音乐(在他的《一种新科学》中提到过)。这种东西他说很多都在“秘密地”搞,往往是替玩对冲基金的金融服务公司弄的。而Wolfram引擎已经为如果做事和展示结果添加了一定程度的智能。

当然这种智能跟人工智能仍相去甚远,但这一天也许会来的。可能是以大规模分布的形式。

Wolfram说,视定义的不同,目前全球大约有100-150亿台计算机,而且很多设备内部也有计算机。在不久的将来,几乎所有的东西都将由计算机组成—甚至很小的东西。到那时,计算的作用甚至比现在还要大,而且那时候各种级别的东西都将是可适配的、可修改的。

Wolfram所指的也许是技术奇点。当我们到达技术奇点时,智能将成为万物唯一的定义因子,而且那时候的技术发展节奏之快已非现在的人们所能理解了,世界变化越来越快,快到人类已经无法想象。

如果这个奇点真的到来,可能就是智能系统发展的结果。也许Wolfram语言就是此类系统的先驱。

也因此Wolfram语言才会如此的难易理解和解释,正如Wolfram在博客中所述:

在我看来,现在还是它将会带来的结果的早期阶段。但我已经可以确定该项目是我们迄今为止最重要的一个。这需要艰苦卓绝的工作,但它所展现的景象会令人无比兴奋。我已经迫不及待,恨不得“即将推出”变成所有地方的人都能使用的实际系统……

除非注明,本站文章均为原创或编译,转载请注明: 文章来自 36氪

36氪官方iOS应用正式上线,支持『一键下载36氪报道的移动App』和『离线阅读』 立即下载!

09 Dec 00:51

优秀的电子邮件加密工具

by WinterIsComing
Alison Xue 写道 "电子邮件仍然是最有用的功能之一,可以与使用任何平台的家人、朋友和同事保持联络。随着电子邮件面临越来越大的威胁,必不可少的安全和加密系统变得日益复杂。防止电子邮件未授权访问和检查尤其重要,因为电子邮件使用协议不包括加密,而电子邮件在设计时也没有特别考虑隐私和加密。缺乏安全的后果是,攻击者可以通过发送者设备、接收方设备、网络和服务器入侵电子邮件。发送加密邮件是保护隐私的一种方法。加密电子邮件听起来有点让人望而却步,但只要正确使用软件(如Thunderbird的加密扩展Enigmail,Google Chrome和Mozilla Firefox的扩展Mailvelope和GnuPG),一切都会很简单。 "
    


09 Dec 00:49

北邮教授:虚拟空间很野蛮 不能用文明手段管理

12月8日消息,中国信息安全漏洞库发起的第六届安全信息漏洞分析及风险评估大会今日在北京举行,北京邮电大学教授杨义仙在大会演讲中提出了目前信息安全管理的六大错误,杨教授称,赛博空间(虚拟空间)是一个返祖的野蛮社会,缺乏基本的道德准则,不能只用文明手段来管理,必须向远古前进由丛林法则开始,推动赛博社会的生态文明建设。
    


05 Dec 07:12

Lua简明教程

by 陈皓

The Programming Language Lua这几天系统地学习了一下Lua这个脚本语言,Lua脚本是一个很轻量级的脚本,也是号称性能最高的脚本,用在很多需要性能的地方,比如:游戏脚本,nginx,wireshark的脚本,当你把他的源码下下来编译后,你会发现解释器居然不到200k,这是多么地变态啊(/bin/sh都要1M,MacOS平台),而且能和C语言非常好的互动。我很好奇得浏览了一下Lua解释器的源码,这可能是我看过最干净的C的源码了。

我不想写一篇大而全的语言手册,一方面是因为已经有了(见本文后面的链接),重要的原因是,因为大篇幅的文章会挫败人的学习热情,我始终觉得好的文章读起来就像拉大便一样,能一口气很流畅地搞完,才会让人爽(这也是我为什么不想写书的原因)。所以,这必然又是一篇“入厕文章”,还是那句话,我希望本文能够让大家利用上下班,上厕所大便的时间学习一个技术。呵呵。

相信你现在已经在厕所里脱掉裤子露出屁股已经准备好大便了,那就让我们畅快地排泄吧……

运行

首先,我们需要知道,Lua是类C的,所以,他是大小写字符敏感的。

下面是Lua的Hello World。注意:Lua脚本的语句的分号是可选的,这个和GO语言很类似

print("Hello World")

你可以像python一样,在命令行上运行lua命令后进入lua的shell中执行语句。

chenhao-air:lua chenhao$ lua
Lua 5.2.2  Copyright (C) 1994-2013 Lua.org, PUC-Rio
> print("Hello, World")
Hello, World
> 

也可以把脚本存成一个文件,用如下命令行来运行。

>lua  file.lua

或是像shell一样运行:

chenhao-air:lua chenhao$ cat hello.lua
#!/usr/local/bin/lua
print("Hello, World")
chenhao-air:lua chenhao$ chmod +x hello.lua
chenhao-air:test chenhao$ ./hello.lua
Hello, World

语法

注释
-- 两个减号是行注释

 

--[[
 这是块注释
 这是块注释
 --]]
变量

Lua的数字只有double型,64bits,你不必担心Lua处理浮点数会慢(除非大于100,000,000,000,000),或是会有精度问题。

你可以以如下的方式表示数字,0x开头的16进制和C是很像的。

num = 1024
num = 3.0
num = 3.1416
num = 314.16e-2
num = 0.31416E1
num = 0xff
num = 0x56

字符串你可以用单引号,也可以用双引号,还支持C类型的转义,比如: ‘\a’ (响铃), ‘\b’ (退格), ‘\f’ (表单), ‘\n’ (换行), ‘\r’ (回车), ‘\t’ (横向制表), ‘\v’ (纵向制表), ‘\\’ (反斜杠), ‘\”‘ (双引号), 以及 ‘\” (单引号)

下面的四种方式定义了完全相同的字符串(其中的两个中括号可以用于定义有换行的字符串)

a = 'alo\n123"'
a = "alo\n123\""
a = '\97lo\10\04923"'
a = [[alo
123"]]

C语言中的NULL在Lua中是nil,比如你访问一个没有声明过的变量,就是nil,比如下面的v的值就是nil

v = UndefinedVariable

布尔类型只有nil和false是 false,数字0啊,‘’空字符串(’\0′)都是true!

另外,需要注意的是:lua中的变量如果没有特殊说明,全是全局变量,那怕是语句块或是函数里。变量前加local关键字的是局部变量。

theGlobalVar = 50
local theLocalVar = "local variable"

控制语句

不多说了,直接看代码吧(注意:Lua没有++或是+=这样的操作)

while循环
sum = 0
num = 1
while num <= 100 do
    sum = sum + num
    num = num + 1
end
print("sum =",sum)
if-else分支
if age == 40 and sex =="Male" then
    print("男人四十一枝花")
elseif age > 60 and sex ~="Female" then
    print("old man without country!")
elseif age < 20 then
    io.write("too young, too naive!\n")
else
    local age = io.read()
    print("Your age is "..age)
end

上面的语句不但展示了if-else语句,也展示了
1)“~=”是不等于,而不是!=
2)io库的分别从stdin和stdout读写的read和write函数
3)字符串的拼接操作符“..”

另外,条件表达式中的与或非为分是:and, or, not关键字。

for 循环
sum = 0
for i = 1, 100 do
    sum = sum + i
end

 

sum = 0
for i = 1, 100, 2 do
    sum = sum + i
end

 

sum = 0
for i = 100, 1, -2 do
    sum = sum + i
end
until循环
sum = 2
repeat
   sum = sum ^ 2 --幂操作
   print(sum)
until sum >1000

函数

Lua的函数和Javascript的很像

递归
function fib(n)
  if n < 2 then return 1 end
  return fib(n - 2) + fib(n - 1)
end
闭包

同样,Javascript附体!

function newCounter()
    local i = 0
    return function()     -- anonymous function
       i = i + 1
        return i
    end
end

c1 = newCounter()
print(c1())  --> 1
print(c1())  --> 2

 

function myPower(x)
    return function(y) return y^x end
end

power2 = myPower(2)
power3 = myPower(3)

print(power2(4)) --4的2次方
print(power3(5)) --5的3次方
函数的返回值

Go语言一样,可以一条语句上赋多个值,如:

name, age, bGay = "haoel", 37, false, "haoel@hotmail.com"

上面的代码中,因为只有3个变量,所以第四个值被丢弃。

函数也可以返回多个值:

function getUserInfo(id)
    print(id)
    return "haoel", 37, "haoel@hotmail.com", "http://coolshell.cn"
end

name, age, email, website, bGay = getUserInfo()

注意:上面的示例中,因为没有传id,所以函数中的id输出为nil,因为没有返回bGay,所以bGay也是nil。

局部函数

函数前面加上local就是局部函数,其实,Lua中的函数和Javascript中的一个德行。

比如:下面的两个函数是一样的:

function foo(x) return x^2 end
foo = function(x) return x^2 end

Table

所谓Table其实就是一个Key Value的数据结构,它很像Javascript中的Object,或是PHP中的数组,在别的语言里叫Dict或Map,Table长成这个样子:

haoel = {name="ChenHao", age=37, handsome=True}

下面是table的CRUD操作:

haoel.website="http://coolshell.cn/"
local age = haoel.age
haoel.handsome = false
haoel.name=nil

上面看上去像C/C++中的结构体,但是name,age, handsome, website都是key。你还可以像下面这样写义Table:

t = {[20]=100, ['name']="ChenHao", [3.14]="PI"} 

这样就更像Key Value了。于是你可以这样访问:t[20],t["name"], t[3.14]。

我们再来看看数组:

arr = {10,20,30,40,50}

这样看上去就像数组了。但其实其等价于:

arr = {[1]=10, [2]=20, [3]=30, [4]=40, [5]=50}

所以,你也可以定义成不同的类型的数组,比如:

arr = {"string", 100, "haoel", function() print("coolshell.cn") end}

注:其中的函数可以这样调用:arr[4]()。

我们可以看到Lua的下标不是从0开始的,是从1开始的。

for i=1, #arr do
    print(arr[i])
end

注:上面的程序中:#arr的意思就是arr的长度。

注:前面说过,Lua中的变量,如果没有local关键字,全都是全局变量,Lua也是用Table来管理全局变量的,Lua把这些全局变量放在了一个叫“_G”的Table里。

我们可以用如下的方式来访问一个全局变量(假设我们这个全局变量名叫globalVar):

_G.globalVar
_G["globalVar"]

我们可以通过下面的方式来遍历一个Table。

for k, v in pairs(t) do
    print(k, v)
end

MetaTable 和 MetaMethod

MetaTable和MetaMethod是Lua中的重要的语法,MetaTable主要是用来做一些类似于C++重载操作符式的功能。

比如,我们有两个分数:

fraction_a = {numerator=2, denominator=3}
fraction_b = {numerator=4, denominator=7}

我们想实现分数间的相加:2/3 + 4/7,我们如果要执行: fraction_a + fraction_b,会报错的。

所以,我们可以动用MetaTable,如下所示:

fraction_op={}
function fraction_op.__add(f1, f2)
    ret = {}
    ret.numerator = f1.numerator * f2.denominator + f2.numerator * f1.denominator
    ret.denominator = f1.denominator * f2.denominator
    return ret
end

为之前定义的两个table设置MetaTable:(其中的setmetatble是库函数)

setmetatable(fraction_a, fraction_op)
setmetatable(fraction_b, fraction_op)

于是你就可以这样干了:(调用的是fraction_op.__add()函数)

fraction_s = fraction_a + fraction_b

至于__add这是MetaMethod,这是Lua内建约定的,其它的还有如下的MetaMethod:

__add(a, b)                     对应表达式 a + b
__sub(a, b)                     对应表达式 a - b
__mul(a, b)                     对应表达式 a * b
__div(a, b)                     对应表达式 a / b
__mod(a, b)                     对应表达式 a % b
__pow(a, b)                     对应表达式 a ^ b
__unm(a)                        对应表达式 -a
__concat(a, b)                  对应表达式 a .. b
__len(a)                        对应表达式 #a
__eq(a, b)                      对应表达式 a == b
__lt(a, b)                      对应表达式 a < b
__le(a, b)                      对应表达式 a <= b
__index(a, b)                   对应表达式 a.b
__newindex(a, b, c)             对应表达式 a.b = c
__call(a, ...)                  对应表达式 a(...)

“面向对象”

上面我们看到有__index这个重载,这个东西主要是重载了find key的操作。这操作可以让Lua变得有点面向对象的感觉,让其有点像Javascript的prototype。(关于Javascrip的面向对象,你可以参看我之前写的Javascript的面向对象

所谓__index,说得明确一点,如果我们有两个对象a和b,我们想让b作为a的prototype只需要:

setmetatable(a, {__index = b})

例如下面的示例:你可以用一个Window_Prototype的模板加上__index的MetaMethod来创建另一个实例:

Window_Prototype = {x=0, y=0, width=100, height=100}
MyWin = {title="Hello"}
setmetatable(MyWin, {__index = Window_Prototype})

于是:MyWin中就可以访问x, y, width, height的东东了。(注:当表要索引一个值时如table[key], Lua会首先在table本身中查找key的值, 如果没有并且这个table存在一个带有__index属性的Metatable, 则Lua会按照__index所定义的函数逻辑查找)

有了以上的基础,我们可以来说说所谓的Lua的面向对象。

Person={}

function Person:new(p)
    local obj = p
    if (obj == nil) then
        obj = {name="ChenHao", age=37, handsome=true}
    end
    self.__index = self
    return setmetatable(obj, self)
end

function Person:toString()
    return self.name .." : ".. self.age .." : ".. (self.handsome and "handsome" or "ugly")
end

上面我们可以看到有一个new方法和一个toString的方法。其中:

1)self 就是 Person,Person:new(p),相当于Person.new(self, p)
2)new方法的self.__index = self 的意图是怕self被扩展后改写,所以,让其保持原样
3)setmetatable这个函数返回的是第一个参数的值。

于是:我们可以这样调用:

me = Person:new()
print(me:toString())

kf = Person:new{name="King's fucking", age=70, handsome=false}
print(kf:toString())

继承如下,我就不多说了,Lua和Javascript很相似,都是在Prototype的实例上改过来改过去的。

Student = Person:new()

function Student:new()
    newObj = {year = 2013}
    self.__index = self
    return setmetatable(newObj, self)
end

function Student:toString()
    return "Student : ".. self.year.." : " .. self.name
end

模块

我们可以直接使用require(“model_name”)来载入别的lua文件,文件的后缀是.lua。载入的时候就直接执行那个文件了。比如:

我们有一个hello.lua的文件:

print("Hello, World!")

如果我们:require(“hello”),那么就直接输出Hello, World!了。

注意:
1)require函数,载入同样的lua文件时,只有第一次的时候会去执行,后面的相同的都不执行了。
2)如果你要让每一次文件都会执行的话,你可以使用dofile(“hello”)函数
3)如果你要玩载入后不执行,等你需要的时候执行时,你可以使用 loadfile()函数,如下所示:

local hello = loadfile("hello")
... ...
... ...
hello()

loadfile(“hello”)后,文件并不执行,我们把文件赋给一个变量hello,当hello()时,才真的执行。(我们多希望JavaScript也有这样的功能(参看《Javascript 装载和执行》))

当然,更为标准的玩法如下所示。

假设我们有一个文件叫mymod.lua,内容如下:

local HaosModel = {}

local function getname()
    return "Hao Chen"
end

function HaosModel.Greeting()
    print("Hello, My name is "..getname())
end

return HaosModel

于是我们可以这样使用:

local hao_model = require("mymod")
hao_model.Greeting()

其实,require干的事就如下:(所以你知道为什么我们的模块文件要写成那样了)

local hao_model = (function ()
  --mymod.lua文件的内容--
end)()

参考

我估计你差不多到擦屁股的时间了,所以,如果你还比较喜欢Lua的话,下面是几个在线文章你可以继续学习之:

关于Lua的标库,你可以看看官方文档:string,  table, math, io, os

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell.cn ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

相关文章

05 Dec 07:11

打造世界上最火的10个产品,成本是多少?

by tips+osgilias@36kr.com(dog潘)

本文由 Courtney Boyd Myers 撰写, 她是 audience.io 的创始人。这篇文章讲述了她和一些网页和移动开发公司、孵化器、顾问公司还有实验室的高管接触,并谈论关于复制现在市场上非常成功的10个产品需要一些怎样的成本及资源。

10万元和一个合适的开发合伙人就能让你一夜暴富么?到底需要多少资源才能复制出像 Twitter 或者 Instagram 一样的产品?现在随着移动开发顾问和各种孵化器的崛起,现在并不缺少那些能够帮助你打造出下一个 web 端或移动端的变革产品的人才。

我们这次采访了一些网页和移动开发公司、孵化器、顾问公司还有实验室的高管,询问了关于打造出我们这一代那些最成功的应用都需要些什么资源。以下是我们的总结,关于打造世界上最成功的10个初创企业需要的成本和时间。

Twitter

Henrik Werdelin,投资公司 Prehype 的管理合伙人,曾投资过 Tradable、Barkbox、FancyHands、Basno 和 Path,他表示打造 Twitter 并不是什么难事,但其中一些细分的功能则需要花一些时间:

用 Ruby on Rails 打造 Twitter 原型并不难,10个小时就够了,甚至一些厉害的开发者所需时间会更短。

这似乎就意味着如果你懂 Rails,那打造下一个 Twitter 的成本几乎为零。如果你不会 Rails,One Month Rails 99美元的课程加上免费的 Heroku 平台,你要是聪明一点一个星期就能学会并打造出产品的原型。

不过 Werdelin 接着又澄清道其实事情没那么简单:“现在完成产品技术层面的打造并不是什么难事,但难处在于完善使用体验层面,如何让交互界面能够清晰地表达出产品的理念才是关键。”

一个没有扩展性的产品根本不值钱,创业早已不再是关于打造产品,而是打造一个企业,这个过程除了产品外,还涉及内部结构、反馈循环、数据分析和用户社区。

因此,如果你想要先推出一个 MVP(Minimum Viable Product,最简可行产品),Werdelin 表示大概需要5-25万美元,主要看你招聘什么语言的开发者和设计师。

Werdelin 觉得打造一个成功的产品就好比开一家夜店:“你需要的不止是一个DJ、舞台和大量的酒,你还需要合适的装潢、气氛和音乐,当然还有调出的鸡尾酒”。

Instagram

“Instagram 相比 Twitter 来说,开发过程要稍微复杂一些,特别是滤镜。但你仍然可以以不算太贵的价格打造出类似的原型,只需要6个月10万到30万的成本。但即使是你在第一天就有100万美元的资金可以花费,也很难再发展到 Instagram 现在的增长速率”,Werdelin 这样说道。

Instagram 的创始人Kevin Systrom 在2010年10约6号开放测试版本后,有大约2.5万的试用用户。两年后,这个应用的增长速率大得惊人,已有将近1.5亿的活跃用户。Systrom 在以近10亿美元的价格将公司卖给 Facebook 之后,也成为了千万富翁。

“运气、时机和社交元素成为了很多创业者意想不到的关键成分”。

Facebook

关于 Facebook 我们询问了 HappyFunCorp 的联合创始人 Ben Schippers,HappyFunCorp 是一个提供建站服务的公司,它的客户包括了:TeePublic、Of A Kind、Postography 和 Yeah TV!。

“如果你让我为你打造一个 Facebook,我需要50万美元和9个月的设计和开发时间。”Schippers 这样说道:“其他人可能会需要100万美元或是更长的时间,但其实这个问题本省就比较泛化,因为钱并不是花在开发上,而是花在运营上。”

当你有了产品的想法之后,50万美元需要平均分配在九个月中,前两个月主要专注于设计,后面六个月则需要将设计转化成产品,剩下的时间则是测试和更新。

拿2002年市场上的服务器提供商来计算,Schippers 表示 Zuckerberg 在 Facebook 的第一年大概每个月要花3000美元在服务器上,而到了2006年,这个数额则变成了1000万美元每个月,因为 Facebook 所设计的数据过于巨型。

考虑到 Facebook 的增长率,Schippers 估计现在 Facebook 光是花费在服务器上的费用就高达每个月3000万美元,这样看来,那些初创企业融资的几百万几千万,看上去就没有那么美好了。

WhatsApp

“为了推出一个类似 WhatsApp 的 MVP 原型,起码基本功能要做好,比如电话号码认证等等,而这需要花费大概12万美元的成本”。Ryan Matzner 解释道,Matzner 是一个移动开发公司 Fueled* 的策略主管,他们的用户包括了 Elevatr、Ribbon、UrbanDaddy、JackThreads 等等。

除了12万美元的打造原型的费用,Matzner 表示还需要另外12万美元来搞定设计和品牌树立,然后再加上2.5万美元的产品测试迭代和稳定性的完善。

所以总共算下来,WhatsApp 光是前期的打造就要花费大概50万美元和9个月的时间。

Uber

作为 Huge 的工程副总裁,Artem Fishman 已经帮着打造了 NYC.gov、Revolt.TV 以及 TED.com 的重新设计。Huge 的团队仔细研究了 Uber 在 CrunchBase 上的各种数据,这家旧金山的公司在花费了5000万美元之后,又在今年8月从 Google 和 Benchmark 拿到了2.58亿美元的融资。Fishman 表示:

当别人询问你打造 Uber 这样的模式至少要多少资源时,我一般的回答都是用不了多少,根据 Uber 早期的情况,一个 MVP 大概需要150万美元左右的资金。

但之后扩张的路则花费巨大,不管是从技术层面还是市场层面来说,但对于 Uber 这种类型的服务来说,当地车辆监管制度的问题远比扩张的花费来得更加重要。

Pinterest

Sam Mathews 觉得图片微博 Pinterest 的核心功能其实算是比较简单。Mathews 是品牌推广公司 Neverbland 的创始人,在他眼中,一个四个人的团队在4个月里就能开发出 Pinterest 的原型,花费只需要12万美元。

不过当 Pinterest 的用户增长到100万到7500万的时候,花费就开始控制不住,这其实和 Pinterest 的规模无关,而是关于它的规模经济的效应。Mathews 估计现在的 Pinterest 大概需要150个员工,并且每个月的花费在近200万美元,才能持续它现在的产品规模。

Shopify

为了复制电子商务平台 Shopify,Mathews 任务花费至少在25万美元和30万美元之间,而且前提是你已经拥有非常厉害的设计师和开发者。

一些比如维护 Shopify 的 API 以及为用户提供建站的服务器之类的花费并没有计算在内,Mathews 表示:

通常一个像 Shopify 一样的产品,你需要在做决定的过程里考虑用户收费和开发支出,但如果你只是完全复制 Shopify 的产品模式的话,你只需要4-6个月的时间。

借用 Paul Graham 的文章《How to Start a Startup》,Mathews 说道:“为了打造一个复杂的产品,你先要打造一个简单的原型,如果你想直接跳过这一步,那是绝对行不通的”。

其实打造一个像 Shopify 一样的产品,问题就在于它是经过了7年时间的不停更新迭代而形成的,这其中夹杂了太多用户反馈和市场变化,就算你真的复制出外表和 Shopify 一样的产品,但其内在的运作模式和系统,却是你永远无法拷贝的。

愤怒的小鸟

这是我们列表上唯一的一个游戏应用,我们邀请了 DJ Saul,iStrategy Labs 的 CMO 来为我们解答相关的疑问。iStrategy Labs 是一家线上顾问中介,帮助打造了像 Grandstand 和 Social Machines 等产品。Saul 表示:

首先你需要把时间成本考虑进去,包括品牌的打造,甚至是 logo 设计颜色搭配等,并且还有非常重要的 UX 用户体验,特别是对于游戏本身。你需要一些很有经验的跨平台游戏设计师和工程师,更别说你进军 Android 平台后测试各种兼容性所需要的成本了。

Saul 预计如果为了复制一个愤怒的小鸟,他需要20个人的团队和超过一年的时间,每人的年工资为11万美元,总共预算220万美元再加上其他费用。更别说如果你想要依靠外面的顾问公司,那这个花费还得增加0.5倍。

Tumblr

根据 Saul 的观点,Tumblr 比愤怒的小鸟更容易复制,你并不需要游戏方向的设计师和开发人员,不过最后的测试更新任务则没有区别。

Tumblr 现在的博客数量达到了1.4亿个,Saul 表示:

最大的问题就在于服务器,另外的开支主要是在产品推广上,我需要大概15个跨平台开发人员以及一年的时间才能完成,平均每人年薪11万美元的话,总共至少需要165万美元。

Vine:到底什么是MVP

当人们已经听惯了“MVP”时,我们对它的定位却有所不同,MVP 其实更算是一个我们认为已经好到能够放到市场上去吸引用户的产品,目的是为了积累前期的用户。

移动用户体验和设计咨询公司 Worry Free Labs 的 CEO Paul Choi 这样说道。为了打造一个 Vine 的 MVP,Choi 表示他估计需要12.5万美元到17.5万美元的资金,和4到6个月的设计开发时间:

对于 Vine 来说,你需要开发一个后端系统来储存用户的视频和一个前端界面来让用户可以很容易地用视频进行社交。

所以那些现在正在考虑打造一款视频产品的话,一定要把服务器的成本考虑进去。但也有一些像 Amazon Web Services 和 Parse 的后端服务提供商,能够帮助你尽快达到4000万用户的阶段。Worry Free 的商业部门副总裁 Brain Badillo 预计像 Vine 这样的产品,在服务器上的成本就超过了5万美元每个月。

总结

上述所有这些 web 端和移动端的产品都已经在市场上生存了很长一段时间,短则几个月,长则数年。对于复制它们到底需要什么成本的计算,其实是非常复杂的,因为这需要精细到设计师、开发者或者是产品经理的招聘上,每一家公司在这方面的策略都不同,所以单单服务器上的成本没有太大的代表性。

另一方面,产品的分发推广也是一笔巨大的开支,但这也非常难估计,每个产品的情况都很不一样。现在创业的台阶其实很低,很多创业者拿着10万美元起家照样过得很好,但他们前期都面临同样一个问题:如何将品牌做出来,并在对的时间找到对的用户,前期的分发渠道一直都是产品成功的关键。Betaworks 的副总裁 Paul Murphy 表示:

山寨固然简单,最难的地方不在于技术层面,而是在于你如何找到一个符合自己的模式,我们可以打造出上述这些产品中任意一个的 MVP,使用现有的开源 SDK、框架和基本的设计,在一个周末的编程马拉松比赛上就能够做出来。

的确,即使你现在有100万美元,你也复制不出 Twitter、Instagram 或者是 Facebook,但你可以在它们的基础上,花个几百万,打造出完全不同的产品,这些是共通的。

作为一个激情四射的创业者,你想要打造出颠覆性的产品,它拥有更新更好的功能,而不是简简单单的复制,这也是下一个 Jack Dorsey(Twitter)、David Karp(Tumblr)、Kevin Systrom(Instagram)和 Mark Zuckerberg(Facebook)会做的事。

“好的创意来自于无数个创意,你仅仅需要把不好的那些扔掉”—— Linus Pauling

除非注明,本站文章均为原创或编译,转载请注明: 文章来自 36氪

36氪官方iOS应用正式上线,支持『一键下载36氪报道的移动App』和『离线阅读』 立即下载!

30 Nov 07:33

撿便宜必備全台購物網比價王, App 幫你行動中隨時省錢

by esor huang
有時候可能我們在賣場、超商、書店或 3C 店家看到一項產品,有點心動想要下手,但是先等等,你怎麼知道現在網路上的購物商城裡不會有更便宜的選擇呢?或許當下先拿出手機,上網搜尋一番,說不定可以找到加上運費還比店家便宜的產品?

又或者真的宅在家裡,打算上網購物,但是現在線上購物網這麼多,同一個產品每一家都在賣,到底哪一家加上運費後最便宜呢?

如果平常你也是覺得要買東西就不要多浪費,既然有很多價位選擇,就選擇一個經濟實惠的,那麼今天要介紹省錢一族可以試試看的 Android、 iPhone、 iPad App:「 雲端找便宜 」,這款 App 內建了全台各大購物、拍賣商城價位搜尋,可以讓你拿出手機「隨時就能比價」,就算不一定要這麼精打細算,但即時的查詢一下價格,也可以讓你知道行情,不至於買太貴。



  • 01.輸入關鍵字,就能開始比價:
「雲端找便宜」同時擁有 Android 和 iPhone、 iPad App,安裝這款 App 後直接在需要時輸入產品關鍵字,就能幫我們上全台各大購物商城,進行價格搜尋、比價。

我自己實際使用時,大多會用在兩種情境:
  • 1.在實體賣場逛街,看到某個產品,打開 App 搜尋一下,看看網路上的價格比較。
  • 2.線上購物時,不用一個一個賣場找,直接搜尋這個 App,找出價格最划算的線上商城。
「雲端找便宜」內建了 Yahoo、 PCHome、 露天、 淘寶等台灣用戶最常使用的商城,因此可以提供足夠完整資訊的比價。




  • 02.設定更精準比價的範圍區間:
在輸入關鍵字,準備正式開始搜尋產品之前,要先選擇比價的範圍,這樣可以讓比價的結果更加精準。

最常用的應該是設定「價格區間」,但這個前提是你對於這個產品的價位有概念。


如果對於產品價位沒有概念,那麼可以使用「常用分類」或「不分類」來搜尋比價,會找出範圍比較廣的產品,但就不需要事先設定價格。

另外,避免找到一些非目標的低價產品(例如搜尋刺客教條,結果不是找到遊戲,找到的是海報),也可以選擇依據「最低價格」以上來搜尋。




  • 03.把搜尋結果暫時收藏:
有時候比價會是一個比較繁瑣的動作,可能要比很多次,這時候為了記住之前找到的一些不錯的產品,可以利用「雲端找便宜」內建的「收藏」功能。


點擊該項目右方的按鈕,就可以彈出「收藏」的選單,以後可以在「收藏列表」裡面看到這個產品,也方便多方的比價。




  • 04.過濾掉「拍賣」或「非24小時到貨」結果:
有時候線上購物時,我不想買拍賣的產品,想直接買商家的貨,那麼我可以在過濾方法中選擇「無拍賣」,過濾掉拍賣的結果。

或者有時候要這個東西很急,那麼可以選擇「24H」,直接只保留可以24小時到貨的搜尋結果。




  • 05.也可以當作購物資訊收集來使用:
因為「雲端找便宜」內建了幾個台灣主要的線上商城,這時候除了比價外,也可以當作一種購物前資訊收集的便利工具。

例如有時候我想買一個手錶當作禮物,大概價位是要 1000 元以上的,但是有哪些手錶符合這個條件呢?我就可以利用「雲端找便宜」輸入「手錶」,設定最低價位「1000」,就可以找出需要的商品品項。

又或者我想買一台 Canon 的相機,有哪些選擇呢?我可以先輸入「 Canon 」,「雲端找便宜」還提供了關鍵字的延伸建議,會直接顯示出 Canon 可以有哪些關鍵字組合,也可以直接找某個價位以上的相機選擇。

無論你真的要購物比價,或者只是想先收集購物情報,看起來「雲端找便宜」都可以幫到我們。最後,當你真的線上購物,那麼別忘了使用「全球快遞追蹤免費版 App 台灣快遞、郵局包裹查詢提醒」,來查詢到貨時間喔!

喜歡這篇文章嗎?歡迎追蹤我的FacebookTwitterGoogle+,獲得更多有趣、實用的數位科技消息,更歡迎直接透過社群聯繫我。

27 Nov 06:41

共享经济的又一方向:自行车智能锁 Lock8 达到 Kickstarter 集资目标后又获不明金额的融资

by tips+osgilias@36kr.com(dog潘)

看来自行车智能锁最近很火,大家都忙着进军汽车共享领域的时候,两家自行车智能锁先后上线 Kickstarter。我们报道过的 Bitlock 在一周前也完成其12万美元的融资目标。昨天一家欧洲的创业公司 VeloLock 推出的 Lock8 也完成了其 Kickstarter 上5万美元的集资目标,共集资8.1万美元,但之后又得到了 Horizon Ventures 和 Otto Capital的追加投资,但金额不明。

Lock8 和 Bitlock 的基本功能都差不多,用户通过使用智能手机控制自行车的锁,并可以随时和身边的人分享虚拟钥匙,这样一种模式甚至还有社交的功能,我们也很形象地称之为“自行车社交”。除此之外,两者在健康监控领域和自行车防盗上的功能也基本相同。

但硬件方面却各有千秋,售价149美元的 Lock8 的电池在行驶时可靠电磁充电,而不需要像售价129美元的 Bitlock 一样换电池,两者均带有GPS定位,可以很方便地进行分享或者防止被偷。

说到偷,一般小偷把锁撬了是不会带着锁一起跑的。Lock8 在这方面采取的措施要更多,除了GPS之外,Lock8 还内置了一个警报器(并与手机的通知相匹配),如果小偷想撬锁的话,其内置的震动感应器就会激活警报,更甚至,如果小偷想要将锁冷冻的话,那 Lock8 还内置了温度传感器,真不知是什么土豪自行车,但即使是这样,Lock8 的售价也不是很贵。

这两个智能锁的公司 Mesh Motion 和 VeloLock 很明显都是看中了共享经济的潜力,通过智能锁可以随时可以与周围人共享你的自行车,也可以提供租赁服务,这样就提高了空闲自行车的利用率。也不知道国内有没有公司在做这个,特别是在大城市,自行车这种代步工具仍然没有衰退的迹象,其实还是挺实用的,但是成本高估计也会拉高价格。


除非注明,本站文章均为原创或编译,转载请注明: 文章来自 36氪

36氪官方iOS应用正式上线,支持『一键下载36氪报道的移动App』和『离线阅读』 立即下载!

26 Nov 05:27

Linux编程女神计划招募新的内核级MM实习生

感谢Mr小眼儿的投递
作为FOSS的Linux编程女神计划(Outreach Program for Women)的一部分,这个夏季在Linux基金会工作的实习生们背景不同,水平也高低不等,但是她们至少有一件事情是相同的(除去性别之外),就是她们可以在自己的履历中添加一项“Linux内核黑客”。
    


22 Nov 02:27

[小红猪]喵星人和它们的奴隶们,不得不说的秘事 Ⅱ

by 小红猪小分队

本文作者:小红猪小分队

译者:亚得里亚海上的猪

原文作者:John Bradshaw

epPxK40cGFsjldn4oyNfru5KfEFz_xPgzHmF2DhSJ36ABwAAOAQAAEpQ_645x362【猫是世界上最受欢迎的宠物,但这恐怕正是它们的生活日益艰难的原因。图片来源:tvnet.lv】

家猫是全世界最受欢迎的宠物,数量是狗的3倍之多。猫咪之所以受人喜爱,有一件事不可不提——它们既待人亲切,又独立自主。它们基本上不需要怎么训练,会自己整理毛发,主人不在时也不怎么思念主人,一旦主人回家,它们又会亲热地迎上前来。一句话,它们可以召之即来、挥之即去。

即便如此,猫仍然是冷淡莫测的。狗是开朗诚实顺从的。猫则不同,需要我们接受一些条件,才能和它们交往——至于是什么条件,它们却从来不说。

我们能了解它们吗?我相信能。我曾和好几只猫咪同在一片屋檐下,但我好像并没有对它们真正有多少了解。相比之下,科学倒是开始揭示出猫咪的一些真实天性,特别是它们和人类的关系。现在,我们该盘点一下自己学到了什么,又该怎样用所学改善猫咪的生活了。

与人类同行

猫和人的交往源远流长。对DNA的研究表明,宠物猫的祖先是阿拉伯的非洲野猫(Felis silvestris lybica),人类对猫的驯养大约始于10000年到15000年前,地点则在中东。

第一个驯养野猫的民族大概是纳图夫人(Natufians),他们在距今13000年至10000年前生活在黎凡特(译注:黎凡特,大致是指地中海以南的中东地区)。纳图夫人是公认的农业的发明者,也因此受到了贪吃谷物的老鼠的困扰。野猫大概就是在这个时候,到人类附近觅食的。纳图夫人一定是发现了野猫的妙用——它们捕食老鼠,对谷物却没有兴趣——因此才鼓励它们待在自己的身边。

但那些并不是我们所说的宠物猫。它们大概更像是今天在城市里出没的狐狸,有能力适应人类环境,却也保留了本身的野性。

当然了,猫的其他特征肯定也被人类注意到了:它们有可爱的外表、松软的毛发,还学会了对我们撒娇。因为这些特征,我们将它们作为宠物豢养了起来。渐渐地,猫咪潜入了人类的居所、人类的心,在数千年的时间里,它们由野猫变成了家猫。

虽然改变了身份,但猫的4只爪子里,还是有3只牢牢地踏在野外。狗的心灵和祖先狼已经天差地远,猫却还是像野生捕猎者一般思考着。

猫不是人类创造的产物。在某一点上,它们和几乎所有的家禽都不相同,那就是它们仍然主宰着自己的生活。大多数猫是想去哪里就去哪里,想什么时候去就什么去;最关键的是,它们还能选择自己的伴侣。和狗不同,猫当中只有一小部分是人类有意培育出来的。在历史上,还没有人培育猫咪来看家、放牧、或者协助狩猎。猫的驯养主要由自然选择驱动。猫挤进了人在无意间开辟的生态位,和人类一同演化着。

由于这些原因,猫不能看作是完全驯化的动物,它们的许多行为还体现着野外的本能。要理解猫咪为什么有这样那样的行为,尤其是它们和我们的关系为什么如此这般,我们必须先理解它们的这些本能才行。

猫可以和人很亲热,但是它们对亲热的对象也很挑剔。这一点可以从它们的演化历史中找到原因:野猫主要是独行的动物,将大多数同类都看作对手。家猫在面对其他猫咪的时候,它们的本能反应也是怀疑,甚至恐惧。

但是,人类的驯养已经使它们的警惕有所放松。要得到人类的驯养是有条件的:首先,它们必须和同类摩肩接踵地生活;其次,它们必须和人类建立感情。为了满足这两个条件,猫发展出了全新的社会技能。

社会行为的演化有可能始于野猫在谷仓外聚集的时候。任何继续对同类显示敌意的野猫,都会在捕鼠的时候落于下风。

即使到了今天,哪里经常有食物出现,哪里就会冒出一群流浪猫来——只要当地的人类许可。群落慢慢建立,直到几百只猫咪密集地生活在一起。

4QrR2NQPCtMgUmzyNbdtQMiPXj5K6AtMKu6l57yj9Jq2BAAAAAQAAEpQ_645x547【竖起尾巴是家猫与家猫之间社交行为的一个重要信号。图片来源:justcats.ru】

学会社交

在这些群落中,社会的基础是彼此有血缘的母猫之间的协作。母猫在生下小公猫后几个月就会把它们赶走,迫使它们独立生活,为的是防止近亲交配。当群落中的家庭不止一个,它们就会相互竞争。群落内部远远谈不上和谐。猫似乎不能与大量同类维持友善关系,也无法像灵长类动物那样建立起家庭之间的联盟。这样复杂的斡旋技巧,并不在它们的能力范围之内。

不过,从独立生活到社会生活的转化,在沟通上需要质的飞跃。猫是一种爪牙锋利的动物,要是没有一套信号系统供它们解读彼此的情绪和意图,一点小小的口角就会升级为一场危险的打斗。而猫也的确演化出了这样一套系统。

就家猫而言,我的研究已经证明,它们最重要的信号是竖起尾巴。在群落之中,当两只猫咪在盘算是否要接近对方时,其中一只往往就会竖起尾巴;如果另外那只也乐意靠近,它就也会竖起尾巴。这个信号十之八九是在人类驯养期间形成的,很可能源于野猫幼崽和母亲打招呼的姿势。成年野猫是不会对彼此竖起尾巴的。

一旦彼此都竖了尾巴,接下来就可能发生两种情况。要么,两只猫咪互相摩挲头部和身体侧面,间或还会摩擦仍然竖着的尾巴,然后分开;要么,它们会梳理对方的毛发,这个行为在许多动物中都具有深刻的社交意义。对猫来说,摩擦和梳毛大概都发挥着同样的作用,那就是巩固彼此的友谊。

当然,要成为宠物,猫咪还必须学会一项最重要的社会技能,那就是与人类的交流。这项技能一定是在和同类的交流中发展出来的——除此之外不可能有别的源头。

猫并不是生来就依恋人类。它们只是在很小的时候,才对人类有一段短暂的信任。

20世纪50年代,研究者在对狗的研究中确立了“关键社会化时期”(primary socialisation period)的概念。这个时期的小狗特别容易学会与人类交流。对狗而言,这个时期是出生后的7周至14周。

这个概念同样适用于猫,但在时间上还要再早一些。小猫如果在出生后的4到8周与人类频繁交往,一般就会在日后发展出对人类的强烈吸引。如果在出生的头10周内都没有遇到人类,这样的猫咪就可能终身怕人。

但是,过了8周的分水岭之后,猫咪并不会就此停止对人类的了解。据我们所知,它们在出生后1年内对于人类的了解,要比头8周多得多。

常有愤世嫉俗者说,猫咪表现出来的亲热都是假的,目的是骗我们提供食宿,而宠物主人只不过是将自己的情绪投射到了宠物身上。但实际上,我们喜爱宠物的理由是站得住脚的。它们的面部特征与人类相似,这是原因之一,但光有这一点肯定不够。猫之所以成为一种成功的宠物,原因在于它演化出了令我们喜欢的交流方式。

即便在人类驯养的早期阶段,猫咪也需要人类在老鼠短缺的时候为它们提供庇护和食物。在那种情况下,谁能用陪伴来回报人类,谁就能够繁衍生息。

UCyR3Xs5WOXFIE5BmFMjwv9neGCQzHA4uR00sClySeNABgAAhAMAAEpQ_645x362【猫咪对人类的依恋很可能也是带有感情的。图片来源:milemoa.com】

真情流露

许多人喜欢猫咪,那么猫咪对我们的感情又如何呢?我们知道,它们有能力喜欢别的猫咪,因此,它们对人类的依恋很可能也是带有感情的。

然而,要证明这一点并不容易,因为我们只能凭借猫的动作来判断它们的感情,而猫并不会把心思都写在爪子上。

许多猫主人说,他们的猫咪用“咕噜”声表示满足。然而“咕噜”的意义并不明确。猫的确会在满足的时候咕噜,而且许多猫儿都会如此。但是一只咕噜的猫也可能是饿了,或者是有点焦躁。有些猫嘴里咕噜,身体语言却显示它们在发怒。偶而还有些猫儿会在感觉悲痛的时候,甚至在临终之际咕噜两声。

因而,咕噜声未必能透露一只猫咪的情绪状态。它更像是行为生态学家所谓的“操控”信号,表达的是这样一个请求——“请到我身边坐下来”。

然而,一些我们常常忽视的信号,倒的确可能是情感的表达。成年猫维系彼此的关系,主要是靠相互舔舐和摩擦。因此我们要研究一下,这些动作是否也体现了对于我们的喜爱之情。

许多猫咪会经常舔舐主人,但科学家还没有研究它们为什么要这么做。最可能的原因在于,它们想要和主人交流一下彼此的关系。这关系的基础一定是喜爱,因为互不喜欢的猫咪是绝对不会为彼此梳理毛发的。不过,在对猫咪梳理彼此毛发的原因有进一步了解之前,对于它们舔舐我们的原因,我们只能猜测。

猫主人还会和猫咪举行一种触觉仪式,那就是抚摸后者。多数猫主人会抚摸猫咪,一是摸起来舒服,二是猫咪也受用。但是除此之外,抚摸对于猫咪还可能具有一些象征意义。多数猫咪喜欢主人摸它们的头部,这也是它们在梳理毛发时触碰的部位。

许多猫咪不是被动地接受抚摸,它们会跳上你的膝盖,或者翻滚身子,以此邀请人的抚摸。它们还会把身体的某一部分对着你,或者改变姿势,以此表示希望哪里被摸。接受抚摸是猫的社交仪式,它能够加深猫咪和主人的感情。

触摸虽然重要,但是猫咪对我们最清晰的示爱多半还是竖起尾巴。猫咪如果竖着尾巴走向主人,往往接着就会到主人的腿上摩擦。摩擦的方式似乎每只猫儿都有所不同,有的只摩擦头部侧面,有的是摩擦身体侧面,还有的是用尾巴触碰。还有许多猫儿根本不碰主人,而是径直走过,去摩擦附近的什么物体。

有时候猫咪摩擦我们,看起来像是在留下气味。但是摩擦时即便有气味的转移,它对猫咪似乎也没有什么特别的作用。它们似乎已经明白,我们对它们留在我们腿上的微弱气息是无动于衷的。因此,猫儿享受的大概是摩擦本身——要不是这样,它们就不会摩擦了。

许多猫儿在即将喂食的时候摩擦得最起劲,有人因此批评它们这纯粹是在献媚。然而实际上,很少有猫儿只在进食前摩擦主人;而且,当两只猫儿互相摩擦的时候,它们也不会得到什么额外的回报。因此,互相摩擦不过是在表达喜爱,没有别的意思。

【许多猫咪不是被动地接受抚摸,它们会跳上你的膝盖,或者翻滚身子,以此邀请人的抚摸。图片来源:ename.cn】

喵乐之声

猫咪和我们交流还有另一种方式,那就是吸引我们的注意,一般是通过喵喵的叫声。喵喵叫是猫天生的能力,但它们很少用这种方式和别的猫沟通,而野猫一般是默不做声的。虽然天生就知道怎么喵喵,但每一只猫咪都要经过学习,才能用喵声建立最有效的沟通。

一旦明白了主人对喵声有所反应,许多猫咪就会发明出一系列叫声,接着通过试验和淘汰,再学会在特定的场合发出特定的声音。假以时日,每只猫咪就会发展出独特的“语言”,这种语言只有这只猫咪和它的主人才懂,别的猫和猫主人是无法理解的。

所以说,猫在和人类交流时是异常灵活的,虽然许多人说它冷淡,但实情恰恰相反。我们可以认为,它们的一些行为是在操控我们,但这种操控不过是两个朋友探讨彼此友情的程度。双方之间的友情无疑是真挚的。

无论如何,我们不能认为和人类讨好关系就是猫的生存目的。比起人,它们对居住的地方有着更加强烈的依恋。喂食良好且做了绝育手术的猫,照理不会有独占领地的要求。尽管如此,大多数猫还是会常常在窝周围的一小块区域巡逻,为了控制这块区域,还会和别的猫打架。是什么使家猫对这种已经没有必要的遗传行为如此坚持?

答案十分简单:宠物猫的行为都是在过去一万年中演化形成的。在“不久”之前,每一只猫都要捕捉猎物,因而也必须守卫一块领地作为自己的猎场。

领地的重要性在一件事上表现得十分突出:许多宠物猫都会“失踪”、外出流浪,哪怕它们得到了很好的照料。我们在大量猫主人(有些地区达到四分之一之多)的言谈中获得了一些头绪——当问起猫是哪来的时,这些人都回答“是它有一天自己来的”。这些都不是野猫,它们是别人的宠物,但是迫切地需要自己的领地。

我们该如何运用这些新知改善猫咪的生活呢?最重要的一点,或许是让猫咪尽可能保留原始的天性——尤其是关系到别的猫时。许多猫咪终生避免和别的猫接触,然而主人却强迫它们和并不信任的同类一起生活,无论是邻居的猫,还是第二只猫。猫咪们发现,避免和同类接触已经越来越难了。猫虽然是世界上最受欢迎的宠物,但是在我看来,这恐怕正是它们生活日益艰难的原因。

关于本文

编译自:《新科学家》,More than a feline: the true nature of cats

本文首发于果壳网(guokr.com)自然控主题站《喵星人和它们的奴隶们,不得不说的秘事 Ⅱ

21 Nov 02:16

Firefox 28将采用新的用户界面

by WinterIsComing
Mozilla发布了Firefox 28的Nightly版,正式引入了新的用户体验Australis。新的界面加人了一系列增强可用性的改变:标签形状从矩形改变为弧线形;当打开多个标签时,非当前活跃标签变淡,用户将能很容易区分当前活跃标签;清理了工具栏,使其对触控设备更友好。Firefox 28将在明年上半年发布。
    


21 Nov 02:15

GitHub遭遇暴力破解攻击,重置用户密码

by WinterIsComing
GitHub报告,攻击者利用约4万个不同IP地址发动暴力破解攻击,轮番尝试破解用户使用的弱密码。GitHub表示,它已经重置了被破解的用户帐号密码,个人访问令牌、OAuth认证和SSH密钥全部撤销。其它受攻击但未被破解的用户密码也被重置。GitHub表示它正展开调查,如果发现源代码或敏感用户信息相关的未授权活动,将会立即通知用户。它建议用户使用强密码和启用二步认证。
    


20 Nov 13:33

Quora精选:5岁熊孩子的提问

by Junius_Lou

Tejeal Bhamre (2.3k票)

Quora精选:5岁熊孩子的提问

以下对话出自三岁的小孩,但是值得一看:

萨拉:粑粑,你刚才在浴室吗?(° ο°)?

爸爸:是啊。⊙▂⊙

萨拉:为啥 (° ο°)?

爸爸:我脏了,洗个澡就干净了呀。

萨拉:为啥 (° ο°)?

爸爸:……为啥洗澡能让我干净?

萨拉:啊。

爸爸:因为我用擦了香皂,水一冲就干净了。

萨拉:为啥 (° ο°)?

爸爸:为啥我要用香皂?

萨拉:啊。

爸爸:因为香皂可以腻走脏东西,让水冲走。

萨拉:为啥 (° ο°)?

爸爸:为啥香皂可以腻走脏东西?

萨拉:啊。

爸爸:因为香皂是一种活性剂。

萨拉:为啥 (° ο°)?

爸爸:为啥香皂是活性剂?

萨拉:啊。

爸爸:这真TM是个好问题。香皂是活性剂是因为它能形成水溶性的胶束微粒,可以融化不溶于水的污物和油脂。

萨拉:为啥 (° ο°)?

爸爸:为啥香皂可以形成水溶性的胶束微粒?

萨拉:啊。

爸爸:因为香皂的微粒是一种长链,具有极性的分子结构,头部一端是亲水性(hydrophilic)的羧酸根离子,尾部是憎水性(hydrophobic)的长链烃基,你会说「hydrophilic」么?

萨拉:爱桌肉乏味渴 (° ο°)?

爸爸:然后,你再来试试 「hydrophobic」 ?

萨拉:爱桌肉乏味渴 (° ο°)?

爸爸:很好!这个单词是防水的意思。

萨拉:为啥 (° ο°)?

爸爸:为什么它是这个意思?

萨拉:啊。

爸爸:这是个希腊词汇!'hydro'的意思是「水」,'phobic'的意思是「害怕什么东西」,连起来就是害怕水的意思。

萨拉:一个怪物 (° ο°)?

爸爸:你是说,和怕怪物一样怕水?

萨拉:啊。

爸爸:没错,一个可怕的怪物。如果你也怕怪物,那么希腊人就会说你有「广场恐惧症」…… ( ⊙ω⊙ 爸爸愣住了)

萨拉:(一 一+) 喂,我们不是在讨论肥皂么……

爸爸:我们就是在讨论肥皂!x__x

( O‘o ……冷场很久…… ⊙ω⊙ )

萨拉:为啥 (° ο°)?

爸爸:为啥一个这种微粒会一头亲水,一头厌水?

萨拉:啊。

爸爸:因为碳氧键是强极性的,而碳氢键是无极性的。

萨拉:为啥 (° ο°)?

爸爸:为什么氧比碳和氢带有更多的负电荷?

萨拉:啊。

爸爸:这就复杂了。这个问题有很多答案,看你和我讨论的是哪个领域的问题,采纳的是鲍林还是密立根的电负性理论体系。鲍林的标度是根据热化学数据和分子的键能来计算其他元素的电负性。而密立根是采用的是电离势与电子亲和计算得来的绝对电负性。但他们最终都归结到有效核电荷作用下。一个氧原子的外层电子的势能要低于碳原子的电子,而且他们之间共享的电荷亲和力的强度要高于氧原子内部的电荷,因此氧原子内的电荷受到了更多来自核电荷的引力,所以原子核也受到了更强的引力!太屌了是吧?

(冷场……)

萨拉:我不太明白啊,粑粑。

爸爸:没关系,我的学生大部分也不明白。

Anna Demer (1k票)

Quora精选:5岁熊孩子的提问
(图片 via shutterstock.com

你会和我结婚吗?

嗯啊……那时候我正在代课,一5岁的傻小子拉着我的手,用漂亮的大眼睛看着我……

我蹲下身,问他,我对他来说,是不是太老了点……

他摇了摇头,对我的爱意在他的眼眸里打转……
我告诉他,最好给我点时间,明天告诉他我的答案(我真以为第二天他就会把这事儿给忘了……)

第二天,他跑过来问我:

如果我不和你结婚了,你能接受么?

我当时就放心了,笑着说没问题,但是我又好奇了。

我问:「为什么你又不要我了?」

他回答: 「我交了个小女友,她才一年级」

Christo Thomas Jose (798票)

第一击

怀孕母亲的儿子:你肚子里装的是啥东西?

母亲:这是你的妹妹,或是弟弟……

儿子:那你干嘛吃了它? o_O

第二击

孩子:你生我之前见过我长啥样么?

妈妈:当然没有!!

孩子:那你把我生出来以后,怎么知道那就是我?o_O

Parkhar Ajabe (251票)

Quora精选:5岁熊孩子的提问
(图片 via shutterstock.com

你什么时候回来?

每次我们在孤儿院做完义工要离开的时,孩子们都想知道……

Christine Leigh Langtree 两个孩子的家长 (199票)

我们到了吗?

Emmet Feerick (132票)

Quora精选:5岁熊孩子的提问
(图片 via shutterstock.com

我朋友的孩子问我的老弟(我们是双胞胎):

你们倆早上醒过来的时候,你怎么知道你就是你自己?

Anne W Zahra (129票)

Quora精选:5岁熊孩子的提问
(图片 via shutterstock.com

娃:最大的数字是多少?

我:没有最大的数字,数字是无尽的。

娃:「无尽」是啥?

我:就是说,这个东西没完没了。数字是没完没了的。

娃:妈,那最「没完没了」的数字是啥?

Cynthia Ward Hemminger (129票)

孩子:上帝吃啥?

我:上帝不吃东西,他不需要,他没有肉身。

孩子:你是说,上帝只有一个脑袋?

[Junius_Lou via Quora]

>>点这里浏览原文

© 煎蛋 / 超载鸡微博 / 图片托管于又拍云存储 / 煎蛋7周年纪念款TEE


    


12 Nov 00:48

穿了一身的淘宝货

by axis

就在我写文章这会儿,淘宝的双11销售额已经超过270亿了。晚上10点左右还会有一个小高峰,因此今天很可能收官于360亿左右。道哥昨天的估计还是保守了点。

 

 

 

至于昨晚有很多朋友信誓旦旦的回复说「500亿」,并表示这是淘宝内部定的目标,我不太能理解这个目标是怎么来的。

 

 

 

就网购狂欢节来说,我已经感到有些审美疲劳了。今年我没有参与抢购,一是因为比较忙没顾上,二是确实「没感觉」了。记得有一年光棍节抢购时,我还非常兴奋的买了好几件衣服。结果拿到手后发现难看到根本就穿不出去,这也是我第一次买了件衣服但一次都没穿过,肠子都悔青了。

 

 

 

说多了都是泪,请看图:

 

 

 

 

 

 

图片来自网络,出处比较杂。Anyway,这深深的教育了我们穿衣服的品位。

 

 

 

现在有句特伤女孩子心的话:

 

 

 

「看那个人,穿了一身的淘宝货。」

 

 

 

不知曾几何时,「淘宝」变成了一个贬义词。一是因为淘宝上的服装「仿」名牌的款仿的特别厉害,但做工和设计却总还是有些瑕疵;二是一些「爆款」的出现,让撞衫几率大增。因此在淘宝上买些「基本款」的衣服还是合适的,但买外套一类的就要谨慎了。

 

 

 

最近和阿里的一些朋友们聊天,感觉他们现在工作压力非常之大。看起来阿里目前似乎是内忧外患,高层很有危机感,所以在前段时间像「百度追求狼性」一样,把压力都爆发了出来。

 

 

 

内忧是阿里的大公司病已经大到了一定的程度,内部各种抢资源,拉山头。大公司病几乎是绝症,大到一定规模后都会得,还没见过治好的。外患是阿里上市遇到阻力,以及来自京东和各大垂直电商的围追堵截。

 

 

 

这些垂直电商如京东、聚美优品、唯品会、一号店、携程、去哪儿、美团等都在成长,他们会越来越大,因此淘宝的份额也必然会受到挤压。从整个电商的盘子来看也许中国电商的规模在变大,因此光从数字上看淘宝仍然是一片繁荣。但从细分的市场份额来看,在家电、化妆品、日用品等方面淘宝的份额也许会逐渐缩小,这会是一个不好的信号。有心人可以留意一下这个现象是否会发生。

 
 

==== 道哥的黑板报 ====

走在创业道路上的文艺白帽子。

微博、知乎:aullik5

http://taosay.net

微信公号:道哥的黑板报,微信ID:taosay

07 Nov 02:05

Debian的默认桌面将换成Xfce

by WinterIsComing
Debian开发者在邮件列表上宣布,下一个版本Jessie的默认桌面将从Gnome换成XFCE。在Jessie开发正式冻结前这一决定还需要通过重新评估。Jessie将在2014年11月冻结开发,桌面评估将从2014年8月开始,如果届时Gnome被认为是一个更好的选择,那么默认桌面将仍然采用Gnome。Xfce是一个轻量级的桌面环境,Gnome 3则因为变化太大而备受争议。
    


31 Oct 12:37

阿里巴巴打响人才大战:毕业生年薪最高60万

北京时间10月30日下午消息,阿里巴巴集团计划新招募1000名毕业生,不仅人数超过去年的5倍,还将提供最高3倍于去年平均水平的薪水。除此之外,由于该公司有可能即将IPO(首次公开招股),所以也加大了对毕业生的吸引力。为了争夺5.9亿网民,以及每年超过2300亿美元的电子商务和网络广告开支,阿里巴巴、腾讯和百度这三大中国互联网巨头今年以来已经累计宣布了35亿美元的交易。而人才则成为了这三大巨头的最新战场。


    


30 Oct 10:31

Cookie末日即将到来,新跟踪系统将取而代之

by WinterIsComing
Cookie的末日可能就要来了。过去一个月,微软、谷歌和Facebook都表示正开发各自的替代跟踪系统。上述行动有可能令市值1200亿美元的全球数字广告行业的力量平衡彻底转变。微软在一篇博客帖子中宣布,对于搭载Windows 8和8.1的平板电脑和PC用户,公司将授权广告营销人员追踪他们的使用信息并向他们发送广告。通过给每一个用户一个“独一无二”的编码,公司可以监控用户在全部应用软件中的活动。业内人士认为,这一编码系统会自然延伸到WP智能手机和游戏机Xbox。今年早前,苹果也开始通过在智能手机及平板电脑上设定唯一的ID,使广告商能跟踪、定位其目标客户群。谷歌上月公布了用于广告跟踪的匿名标识符AdID。但行业专家称这一追踪技术的触手能够伸得很远,可能会将其公司全部产品的用户信息收集到一起——包括Gmail,Chrome以及Android操作系统。
    


30 Oct 10:31

中共中央将遴选100名具冲击诺贝尔奖潜力人才

by WinterIsComing
官方《人民日报》报导,在千人计划(海外高层次人才引进计划)实施五年之后中国开始推出万人计划。这一计划准备用10年左右时间,遴选支持1万名高层次人才。计划包括3个层次7类人才,其中第一层次100名,为具有冲击诺贝尔奖、成长为世界级科学家潜力的杰出人才。这些人才将会获得特殊支持,如安排每人约100万元用于自主选题研究。已进入万人计划的科学家包括:中科院院士刘忠范、薛其坤、周忠和....等等。
    


29 Oct 08:52

维修贵升级难 13英寸视网膜版MBP拆解

前不久,苹果正式更新了配备视网膜屏幕的MacBook Pro产品线。相较于去年的老款,此次新品完整保留了前作的外观,内部升级为Haswell架构第四代智能Intel酷睿双核(13英寸)以及四核(15英寸)处理器,引入Intel Iris锐炬家族核芯显卡,同时还为高配版15英寸机型配备了GT 750M独立显卡,性能有着明显提升。


    


23 Oct 08:05

顺丰拟招2000大学生:月入八万也是有的

Ernest

wa ca

顺丰快递昨天在浙大玉泉校区开校园招聘宣讲会。作为中国最大的快递公司之一,2014年顺丰校园招聘将狂招2000多人。宣讲会吸引了不少学生参加。宣讲会大门口放着展板,介绍顺丰快递:顺丰现有20万员工。一万多辆送货车,40多架货运飞机。除了国内业务,目前还开通韩国、新加坡、马来西亚、日本及美国等国际快递业务。


    


19 Oct 01:38

甲骨文营收超IBM成全球第二大软件公司

本周三,IBM公布了2013年第三季度财报。财报显示,该季IBM净利润40亿美元,同比增6%;总营收273亿美元,同比降4%,因营收未及分析师预期,该公司当日盘后股价大幅下滑。而IBM的软件业务规模缩水,也使之不得不将全球营收第二的软件公司宝座让位给另一家软件巨头,甲骨文。


    


17 Oct 03:29

发现古化石中蚊子体内的血

by oioi

发现古化石中蚊子体内的血
虽然克隆恐龙还是没有多少希望(旧闻《无法从琥珀化石中取得恐龙DNA》),但是这个新闻至少可以让科幻迷们high 一下。电影《侏罗纪公园》里的预言竟然实现了。

来自美国的科学家们在一块蚊子化石上发现了保存完好的,4600万年前的血粉。科学家们说:血粉中的DNA 很可能已经分解了。但这也是人类的首次发现。

来自美国自然历史国家博物馆的科学团队发现了这块化石,他们非常惊讶其中的蚊子胃部的血块竟然保存得非常完好,在变成化石之前这些血块没有被分解。

他们在显微镜下仔细的查看了这个血细胞,发现可能是爬行动物或者鸟类的血迹。

这块化石是在Bozeman 发现的,这个附近也曾发现过霸王龙。[oioi via mosquito]

>>点这里浏览原文

© 煎蛋 / 超载鸡微博 / 图片托管于又拍云存储 / 煎蛋7周年纪念款TEE