{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Sort algorithms"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<font color=RED>Warning: this topic is tough. \n",
    "<br>Read the algorithms several times and really work out examples with pencil on paper. I know that you are allergic to pencil and paper, but I had to do like this several decades ago in order to understand them (no, not because computers hadn't been invented at that time...) and I am sure it will help also you a lot."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Probably you have already written a sorting algorithm. It is not easy to come up with a strategy, but there are really many and I am sure that sooner or later in your life you had to sort something manually. The first algorithm which comes to my mind it this one: find the first element (be it the smallest if you are sorting numbers or the first in alphabetical order if you are dorting text) and put it in the new list in the first position. Repeat the process. \n",
    "<p>This algorithm has a complexity of $N + (N-1) + (N-2) + (N-3) + ... + 2 + 1$ comparisons which is $\\frac{N(N+1)}{2}$ comparisons, so approximately $N^2$ comparisons. Not bad! But we can do better."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Bubble Sort"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Bubble sort algorithm works as if numbers were bubbles in the water. I do not know what is your experience with diving, but I assure you that the largest bubbles go up much faster than the small ones. We thus simulate this process going through the list and pushing the large numbers faster to the right, while automatically the small ones will end up on the left. Let's see how!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The algorithm goes through the full list and compares each element with the next one. E.g. we have\n",
    "<br>[4,7,2,5,1]\n",
    "<br>If the element is larger, it inverts the two elements. If the element is smaller, it leaves the elements as they are. \n",
    "<p>So in our example list we compare 4 with 7 and leave them as they are, then we compare 7 with 2 and swicth them obtaining [4,2,7,5,1] and then [4,2,5,7,1] and finally [4,2,5,1,7]. \n",
    "<br>After this first pass, you can clearly see that the largest element has arrived at the end of the list and thus we do not need to touch it anymore. \n",
    "<p>We repeat the same but this time going from the first element up to the second-last one (remember that the last one is already fine!). \n",
    "<br>Starting from [4,2,5,1,7] we get [2,4,5,1,7] and then [2,4,5,1,7] and finally [2,4,1,5,7] (remember that we arrive only to the second-last position).\n",
    "<br>After this second pass we will have the second-largest element which is in the second-last position of the list, so it's fine. \n",
    "<p>We go through the list a third time up to the third-last element and:\n",
    "<br>starting from [2,4,1,5,7] we remain with [2,4,1,5,7] and then [2,1,4,5,7]. Now also the 4 is in its right position.\n",
    "<p>Guess how does it go on? \n",
    "<br>starting from [2,1,4,5,7] we get [1,2,4,5,7]\n",
    "<p>Now we have only the first element, which does not need to be compared with anything."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's build the code. First we build a function which swicthes the elements of a list and then the real function. Pay attention that indexes in this code can be rather tricky."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0\n",
      "1\n",
      "2\n",
      "3\n",
      "[4, 2, 5, 1, 7]\n"
     ]
    }
   ],
   "source": [
    "def switch(L,a,b):\n",
    "    # this function switches a with b in L\n",
    "    print(\"I am switching \",a,b,\" values: \" L[a],L[b])\n",
    "    L[a],L[b]=L[b],L[a]\n",
    "    # we do not need to return anything, our only scope is to change L\n",
    "\n",
    "def bubbleSort_firstPass(L):\n",
    "    i=0\n",
    "    while i<len(L)-1: # pay attention -1 because last element cannot be compared with something out of list bounds\n",
    "        print(i)\n",
    "        if L[i]>L[i+1]:\n",
    "            switch(L,i,i+1)\n",
    "        i=i+1\n",
    "    return L\n",
    "\n",
    "print(bubbleSort_firstPass([4,7,2,5,1]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "finish  4\n",
      "I compare  0 1\n",
      "I compare  1 2\n",
      "I compare  2 3\n",
      "I compare  3 4\n",
      "finish  3\n",
      "I compare  0 1\n",
      "I compare  1 2\n",
      "I compare  2 3\n",
      "finish  2\n",
      "I compare  0 1\n",
      "I compare  1 2\n",
      "finish  1\n",
      "I compare  0 1\n",
      "[1, 2, 4, 5, 7]\n"
     ]
    }
   ],
   "source": [
    "def switch(L,a,b):\n",
    "    # this function switches a with b in L\n",
    "    L[a],L[b]=L[b],L[a]\n",
    "\n",
    "def bubbleSort(L):\n",
    "    finish=len(L)-1\n",
    "    while finish>0:\n",
    "        print(\"finish \",finish)\n",
    "        i=0\n",
    "        while i<finish:\n",
    "            print(\"I compare \",i,i+1)\n",
    "            if L[i]>L[i+1]:\n",
    "                switch(L,i,i+1)\n",
    "            i=i+1\n",
    "        finish=finish-1\n",
    "    return L\n",
    "\n",
    "print(bubbleSort([4,7,2,5,1]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Merge Sort"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Merge Sort is an algorithm that uses a **divide and conquer** strategy to be very efficient, invented by John Von Neumann in 1945. It starts from the observation that if you have two sorted lists, it is easy to combine them into a sorted one. You just go through the elements of both lists in parallel and choose which one is larger and add it to the final list."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Merge function"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Suppose that we have [2,6,8,9,10] and [3,5,6]. To merge them into a sorted common list, we keep an index of both lists: a=0 (which corresponds to 2) and b=0 (which corresponds to 3). We take the smallest one (2) and put it into a list and consequently increment a.\n",
    "<p>Now we have a=1 (6) and b=0 (3). The smallest one is 3, so we put it into a list and advance b.\n",
    "<p>Now we have a=1 (6) and b=1 (5). The smallest one is 5, so we put it into a list and advance b.\n",
    "<p>Now we have a=1 (6) and b=2 (6). The smallest one is any of the two, so we put it into a list and advance a or b, doesn't matter. Let's take b.\n",
    "<p>Now we have a=1 (6) and b is over!!! At this point we take the remaining part of the first list [6,8,9,10] and attach it to our final list."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Algorithm"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's start with the algorithm. Be ready to reverse your standard way of thinking because this is an algorithm which never sorts!!!\n",
    "<p>The algorithm first splits the list in two lists (<b>divide</b>) and analyses those two lists separately. \n",
    "<br>In our example [4,7,2,5,1] is split into [4,7,2] and [5,1].\n",
    "<p>Then each one is split into two lists which are anaysed separately. \n",
    "<br>So we have ( [4,7] and [2] ) , ( [5] and [1] )\n",
    "<p>Each one is split into two lists which are anaysed separately. \n",
    "<br>So we have ( ( [4] and [7] ) and [2] ) ,  ( [5] and [1] )\n",
    "<p>When we arrive at a list with only one element, that list is automatically sorted and we are happy (<b>conquer</b>). We can now use an appropriate join function to rejoin them:\n",
    "<br>Starting from: ( ( [4] and [7] ) and [2] ) ,  ( [5] and [1] )\n",
    "<br> we join [4] and [7] getting: ( [4,7] and [2] ) ,  ( [1,5] )\n",
    "<br>we join the things between parentheses getting: [2,4,7] , [1,5]\n",
    "<br>and finally we join the two lists getting: [1,2,4,5,7]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Easy, isn't it? :-)\n",
    "\n",
    "Please note that the algorithm never sorts anything! The only way in which it sorts is when joining two lists."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Le's see another example depicted graphically!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"http://www.paolocoletti.it/algorithmicthinking/images/mergeSort.jpg\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's write the code. First of all we need a function which merges two ordered lists. Or better, if you look at the graphical example above, it joins two contiguous ordered pieces of the original list."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[1, 2, 4, 4, 6, 7, 7, 9, 5]\n"
     ]
    }
   ],
   "source": [
    "def merge(L,left,mid,right):\n",
    "    \n",
    "    # this function merges lists from L. First list goes from left to mid included, second list goes from mid+1 to right included\n",
    "    a=left\n",
    "    b=mid+1\n",
    "    R=[]\n",
    "    while a<=mid and b<=right:\n",
    "        if L[a]<L[b]:\n",
    "            R.append(L[a])\n",
    "            a=a+1\n",
    "        else:\n",
    "            R.append(L[b])\n",
    "            b=b+1\n",
    "    # wehn we arrive here, one of the two lists is over!\n",
    "    if a<=mid: # first list still has some elements left\n",
    "        R=R+L[a:(mid+1)]\n",
    "    else: \n",
    "        R=R+L[b:(right+1)]\n",
    "    # at this point in R we have the sorted list and we have to overwrite that part of L with R\n",
    "    L[left:(right+1)]=R\n",
    "\n",
    "L=[1,4,7,9,2,4,6,7,5]\n",
    "merge(L,0,3,7) # we merge [1,4,7,9] with [2,4,6,7] and leave 5 alone there\n",
    "print(L)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then since mergeSort continously calls the splitting, we use recursion! Better putting several print for understanding what is going on."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "mergeSort called  [8, 2, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 0 12\n",
      "We are calling mergeSort first time  [8, 2, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 0 6\n",
      "mergeSort called  [8, 2, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 0 6\n",
      "We are calling mergeSort first time  [8, 2, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 0 3\n",
      "mergeSort called  [8, 2, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 0 3\n",
      "We are calling mergeSort first time  [8, 2, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 0 1\n",
      "mergeSort called  [8, 2, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 0 1\n",
      "We are calling mergeSort first time  [8, 2, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 0 0\n",
      "mergeSort called  [8, 2, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 0 0\n",
      "We are calling mergeSort second time  [8, 2, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 1 1\n",
      "mergeSort called  [8, 2, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 1 1\n",
      "We are calling merge  [8, 2, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 0 0 1\n",
      "We are calling mergeSort second time  [2, 8, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 2 3\n",
      "mergeSort called  [2, 8, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 2 3\n",
      "We are calling mergeSort first time  [2, 8, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 2 2\n",
      "mergeSort called  [2, 8, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 2 2\n",
      "We are calling mergeSort second time  [2, 8, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 3 3\n",
      "mergeSort called  [2, 8, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 3 3\n",
      "We are calling merge  [2, 8, 1, 0, 1, 2, 12, 7, 11, 9, 9, 8, 10] 2 2 3\n",
      "We are calling merge  [2, 8, 0, 1, 1, 2, 12, 7, 11, 9, 9, 8, 10] 0 1 3\n",
      "We are calling mergeSort second time  [0, 1, 2, 8, 1, 2, 12, 7, 11, 9, 9, 8, 10] 4 6\n",
      "mergeSort called  [0, 1, 2, 8, 1, 2, 12, 7, 11, 9, 9, 8, 10] 4 6\n",
      "We are calling mergeSort first time  [0, 1, 2, 8, 1, 2, 12, 7, 11, 9, 9, 8, 10] 4 5\n",
      "mergeSort called  [0, 1, 2, 8, 1, 2, 12, 7, 11, 9, 9, 8, 10] 4 5\n",
      "We are calling mergeSort first time  [0, 1, 2, 8, 1, 2, 12, 7, 11, 9, 9, 8, 10] 4 4\n",
      "mergeSort called  [0, 1, 2, 8, 1, 2, 12, 7, 11, 9, 9, 8, 10] 4 4\n",
      "We are calling mergeSort second time  [0, 1, 2, 8, 1, 2, 12, 7, 11, 9, 9, 8, 10] 5 5\n",
      "mergeSort called  [0, 1, 2, 8, 1, 2, 12, 7, 11, 9, 9, 8, 10] 5 5\n",
      "We are calling merge  [0, 1, 2, 8, 1, 2, 12, 7, 11, 9, 9, 8, 10] 4 4 5\n",
      "We are calling mergeSort second time  [0, 1, 2, 8, 1, 2, 12, 7, 11, 9, 9, 8, 10] 6 6\n",
      "mergeSort called  [0, 1, 2, 8, 1, 2, 12, 7, 11, 9, 9, 8, 10] 6 6\n",
      "We are calling merge  [0, 1, 2, 8, 1, 2, 12, 7, 11, 9, 9, 8, 10] 4 5 6\n",
      "We are calling merge  [0, 1, 2, 8, 1, 2, 12, 7, 11, 9, 9, 8, 10] 0 3 6\n",
      "We are calling mergeSort second time  [0, 1, 1, 2, 2, 8, 12, 7, 11, 9, 9, 8, 10] 7 12\n",
      "mergeSort called  [0, 1, 1, 2, 2, 8, 12, 7, 11, 9, 9, 8, 10] 7 12\n",
      "We are calling mergeSort first time  [0, 1, 1, 2, 2, 8, 12, 7, 11, 9, 9, 8, 10] 7 9\n",
      "mergeSort called  [0, 1, 1, 2, 2, 8, 12, 7, 11, 9, 9, 8, 10] 7 9\n",
      "We are calling mergeSort first time  [0, 1, 1, 2, 2, 8, 12, 7, 11, 9, 9, 8, 10] 7 8\n",
      "mergeSort called  [0, 1, 1, 2, 2, 8, 12, 7, 11, 9, 9, 8, 10] 7 8\n",
      "We are calling mergeSort first time  [0, 1, 1, 2, 2, 8, 12, 7, 11, 9, 9, 8, 10] 7 7\n",
      "mergeSort called  [0, 1, 1, 2, 2, 8, 12, 7, 11, 9, 9, 8, 10] 7 7\n",
      "We are calling mergeSort second time  [0, 1, 1, 2, 2, 8, 12, 7, 11, 9, 9, 8, 10] 8 8\n",
      "mergeSort called  [0, 1, 1, 2, 2, 8, 12, 7, 11, 9, 9, 8, 10] 8 8\n",
      "We are calling merge  [0, 1, 1, 2, 2, 8, 12, 7, 11, 9, 9, 8, 10] 7 7 8\n",
      "We are calling mergeSort second time  [0, 1, 1, 2, 2, 8, 12, 7, 11, 9, 9, 8, 10] 9 9\n",
      "mergeSort called  [0, 1, 1, 2, 2, 8, 12, 7, 11, 9, 9, 8, 10] 9 9\n",
      "We are calling merge  [0, 1, 1, 2, 2, 8, 12, 7, 11, 9, 9, 8, 10] 7 8 9\n",
      "We are calling mergeSort second time  [0, 1, 1, 2, 2, 8, 12, 7, 9, 11, 9, 8, 10] 10 12\n",
      "mergeSort called  [0, 1, 1, 2, 2, 8, 12, 7, 9, 11, 9, 8, 10] 10 12\n",
      "We are calling mergeSort first time  [0, 1, 1, 2, 2, 8, 12, 7, 9, 11, 9, 8, 10] 10 11\n",
      "mergeSort called  [0, 1, 1, 2, 2, 8, 12, 7, 9, 11, 9, 8, 10] 10 11\n",
      "We are calling mergeSort first time  [0, 1, 1, 2, 2, 8, 12, 7, 9, 11, 9, 8, 10] 10 10\n",
      "mergeSort called  [0, 1, 1, 2, 2, 8, 12, 7, 9, 11, 9, 8, 10] 10 10\n",
      "We are calling mergeSort second time  [0, 1, 1, 2, 2, 8, 12, 7, 9, 11, 9, 8, 10] 11 11\n",
      "mergeSort called  [0, 1, 1, 2, 2, 8, 12, 7, 9, 11, 9, 8, 10] 11 11\n",
      "We are calling merge  [0, 1, 1, 2, 2, 8, 12, 7, 9, 11, 9, 8, 10] 10 10 11\n",
      "We are calling mergeSort second time  [0, 1, 1, 2, 2, 8, 12, 7, 9, 11, 8, 9, 10] 12 12\n",
      "mergeSort called  [0, 1, 1, 2, 2, 8, 12, 7, 9, 11, 8, 9, 10] 12 12\n",
      "We are calling merge  [0, 1, 1, 2, 2, 8, 12, 7, 9, 11, 8, 9, 10] 10 11 12\n",
      "We are calling merge  [0, 1, 1, 2, 2, 8, 12, 7, 9, 11, 8, 9, 10] 7 9 12\n",
      "We are calling merge  [0, 1, 1, 2, 2, 8, 12, 7, 8, 9, 9, 10, 11] 0 6 12\n",
      "[0, 1, 1, 2, 2, 7, 8, 8, 9, 9, 10, 11, 12]\n"
     ]
    }
   ],
   "source": [
    "def mergeSort(L,left,right):\n",
    "    print(\"mergeSort called \",L,left,right)\n",
    "    # this function applies mergeSort to L from left to right included\n",
    "    # clearly the FIRST TIME you call it, you must call it from 0 to len(L)-1\n",
    "    if left<right: # it means that our list has more than 1 element\n",
    "        mid=int((left+right)/2)\n",
    "        print(\"We are calling mergeSort first time \",L,left, mid)\n",
    "        mergeSort(L,left,mid)\n",
    "        print(\"We are calling mergeSort second time \",L,mid+1, right)\n",
    "        mergeSort(L,mid+1,right)\n",
    "        print(\"We are calling merge \",L,left,mid,right)\n",
    "        merge(L,left,mid,right)\n",
    "\n",
    "L=[8,2,1,0,1,2,12,7,11,9,9,8,10]\n",
    "mergeSort(L,0,len(L)-1)\n",
    "print(L)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Complexity using int divisions"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "$N=\\text{len}(L)$\n",
    "\n",
    "We do divisions only in the mergeSort part. We have here $\\log_2(N)$ steps, but how many divisions do me make in each step? We do $1$ division at first step, $2$ at second, $4$ at third step, $8$ at fourth step... $N/2$ divisions at last step. We could calculate this sum (which is approx $(\\log_2(N))^3/6$) but it is not worth the effort and not required for you to know. We roughly estimate the maximum amount of divisions per step with $N/2$ and therefore conclude that maximum we are doing $N/2\\cdot\\log_2(N)$ divisions.\n",
    "\n",
    "This is the same in the worst and average and best cases, since we split regardless of the values in the list."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Complexity using comparisons"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "$N=\\text{len}(L)$\n",
    "\n",
    "We observe that each merge does an amount of comparisons that is not larger than the sum of the elements of the two little \n",
    "lists that it has to merge. So at each step all the comparisons that are necessary for that step are no more than $N$. Since we do $\\log_2(N)$ steps, we do maximum $N\\cdot \\log_2(N)$ comparisons!\n",
    "\n",
    "In the best case the list is already sorted and every time we do a merge we just do half of the necessary comparisons, since the second little list will simply be appended. So it is $N/2\\cdot \\log_2(N)$. In the average case we can estimate $3N/4\\cdot \\log_2(N)$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
