6

Click here to load reader

Linked Lists (cont'd) Singly Linked List Runtimes Worst Case

Embed Size (px)

Citation preview

Page 1: Linked Lists (cont'd) Singly Linked List Runtimes Worst Case

Linked  Lists  (cont'd)    

Singly  Linked  List  Runtimes       Worst  Case   Expected  

Case  Best  Case  

size() O(1)      set(int, E) O(n)   O(n)   O(1)  (near  the  front  /  last*)  E get(int) O(n)   O(n)   O(1)  (near  the  front  /  last*)  add(E) O(n)   O(n)   O(1)  *  add(int, E) O(n)   O(n)   O(1)  (near  the  front  /  last*)  remove(E) O(n)   O(n)   O(1)  )  (near  the  front)  remove(int, E) O(n)   O(n)   O(1)  (near  the  front)    *  Can  get  last,  set  last,  add  after  last  in  O(1)  time  if  you  also  maintain  a  reference  to  the  last  node  (aka,  tail),  in  addition  to  the  reference  to  the  first  node  (aka,  head).      How  is  the  space  requirement  for  a  singly  linked  list  compare  to  a  dynamic  array?    Each  node  in  a  singly  linked  list  has  two  references,  one  to  the  data  and  the  other  to  the  next  node.    A  dynamic  array  has  only  one  reference  to  the  data.    But  immediately  after  doubling  the  array,  the  space  is  the  same  as  the  singly  linked  list.    The  worst-­‐case  space  is  the  same.      When  should  you  create  a  new  node?    Only  create  a  new  node  when  you  want  to  add  a  new  element  to  the  list.  You  should  reuse  nodes  when  possible  (eg,  reuse  dominoes).    Writing  the  following  is  wasteful:    

Node<E> temp = new Node<E>(null, null); temp = head;

 It  creates  a  new  node  and  then  immediately  throws  the  new  node  away.    Creating  the  new  node  is  a  relatively  expensive  operation  compared  to  an  assignment.    Simply  declare  the  variable  as  a  Node<E>  without  using  new:    

Node<E> temp; temp = head;

   

Page 2: Linked Lists (cont'd) Singly Linked List Runtimes Worst Case

Circular  Linked  Lists:    The  last  node  in  the  list  points  the  first  node  in  the  list.    You  still  have  a  reference  to  the  head.    

• Useful  when  you  want  to  be  able  reach  every  node  starting  from  any  node.    E.g.,        When  an  operating  system  wants  to  make  it  appear  as  though  several  processes  

are  running  concurrently,  it  will  cycle  through  all  the  processes  repeatedly  and  run  each  one  for  a  short  time.    It  can  use  a  circular  linked  list  so  it  can  remove  and  add  new  processes  to  the  list  quickly,  while  cycling  from  one  process  to  the  next.  

   Doubly  Linked  Lists:    Each  node  contains  the  reference  to  the  data,  a  reference  to  the  next  node,  and  a  reference  to  the  previous  node.    

class Node<E> { E data; Node<E> prev; Node<E> next; }  

 What  does  the  last  node's  next  field  contain?    null    What  does  the  first  node's  prev  field  contain?  null    In  addition  to  keeping  a  reference  to  the  first  (head),  keep  a  reference  to  the  last  (tail)  node.    When  the  list  is  empty,  to  what  should  the  head  and  tail  refer?    null      Advantages:    • You  can  traverse  the  linked  list  in  both  directions.      • If  you  have  a  direct  reference  to  a  node,  you  can  remove  the  node  or  insert  a  new  node  before  it.    

 What  would  you  have  to  do  if  the  list  was  singly  linked?  Start  at  the  first  node  and  traverse  the  linked  list  until  you  find  the  previous  node.        To  remove  a  node  of  a  doubly  linked  list  you  would  include  code  such  as  

Page 3: Linked Lists (cont'd) Singly Linked List Runtimes Worst Case

     node.prev.next = node.next; node.next.prev = node.prev;

 • You  can  remove  the  last  node  in  O(1)  time.        What  is  the  runtime  for  singly  linked  lists?    O(n).  Even  if  you  have  a  reference  to  the  last  node,  or  to  the  second  to  last  node,  you  can't  continue  to  remove  the  last  node,  as  you  cannot  get  to  the  previous  node  in  O(1)  time.  

 • Provides  redundancy.    E.g.,  Used  by  the  IBM  360  to  maintain  directory  structures.    If  the  system  was  modifying  the  linked  list  and  the  system  crashed  the  linked  list  could  be  corrupted.    If  a  node  had  a  bad  next  pointer,  the  system  could  traverse  the  linked  list  backwards  and  if  it  found  a  previous  pointer  to  the  node,  it  could  fix  the  next  pointer:      

Disadvantages:                          • Uses  more  space  than  a  singly-­‐linked  list.    

• More  complicated  code  to  maintain.        Java's  LinkedList  class  uses  a  doubly-­linked  list.    

Page 4: Linked Lists (cont'd) Singly Linked List Runtimes Worst Case

Stacks  &  Queues    Abstract  Data  Type  (ADT):  A  collection  of  data  and  associated  operations  that  can  be  preformed  on  the  data  independent  of  the  implementations.    Specifies  WHAT  the  operations  can  do,  but  not  HOW  it  does  it.    IDEA:  Define  abstract  data  types  that  have  limited  operations  that  are  fast  (i.e.,  easy  to  build  in  software  or  hardware.)    Stacks  and  queues  are  two  such  data  types  with  limited  operations.    From  a  computer  scientist  or  mathematical  point  of  view,  it  is  interesting  to  determine  just  how  expressive  these  simple  data  types  are.      

Stacks    

Operations:  push(E)  -­‐  Put  an  element  on  top  of  the  stack.  E pop()  -­‐  Remove  the  element  at  top  of  the  stack.  boolean isEmpty()  -­‐  Return  true  if  the  stack  has  no  element.    Optional  operations:  E peek()  -­‐  Get  element  at  top  of  the  stack  without  removing  it.  int size()-­‐  Get  the  number  of  elements  on  the  stack.  

 Properties:  • Elements  are  stored  in  order  of  insertion.  (But  you  cannot  refer  to  them  with  an  index,  though.)  

• Elements  removed  in  reverse  order  from  which  they  were  added.  • Last-­in  First-­out  (LIFO) The  last      

Applications  of  a  stack:  • Reversing  order  of  data  • Tracking  function  calls  (system  stack).    • Or  to  mimic  recursion  by  using  an  explicit  stack,  instead  of  the  (implicit)  system  stack.  

• Nested  parentheses  matching  

Page 5: Linked Lists (cont'd) Singly Linked List Runtimes Worst Case

You  cannot  loop  over  a  stack  using  an  index.    Typically,  you  would  write  the  following  coding  pattern:    

while (!stack.isEmpty()) { … x = stack.pop(); … }

   Example:  How  would  you  find  the  maximum  on  the  stack?    Need  to  examine  every  value.  The  look  at  a  value  below  the  top,  you  need  to  pop  each  value  off  the  stack.     // BAD public static void max(Stack<Integer> s) { int maxValue = s.pop(); while (!s.isEmpty()) { int value = s.pop(); if (value > max) max = value; } return max; } What  is  wrong?    The  contents  of  the  stack  are  lost.          You  need  to  save  the  contents  of  the  stack  as  you  examine  the  stack  contents.    Then  push  the  saved  data  back  onto  the  stack.             // Good public static void max(Stack<Integer> s) { Stack<Integer> save = new ArrayStack<Integer>(); int maxValue = s.pop(); save.push(maxValue); while (!s.isEmpty()) { int value = s.pop(); save.push(value); if (value > max) max = value; } return max; }  

Page 6: Linked Lists (cont'd) Singly Linked List Runtimes Worst Case

Example:    Check  whether  a  string  of  parenthesis,  braces,  or  brackets  are  properly  nested.    Used  to  check  whether  programs  have  properly  nested  braces  and  mathematical  expressions  have  properly  next  parenthesis.    eg,    {[()]()}  is  properly    nested.    [{(})] is  not  properly  nested,  because  the  pair  {}  contains  a  single  (                                            and  the  pair  ()  contains  a  single  }.    We  will  refer  to  the  symbols  {[( as  "opening  braces"  and  }])  as  "closing  braces"    Algorithm:    • Read  each  symbol  left  to  right  

• If  the  next  symbol  is  an  opening  brace,  push  it  on  stack  • If  it  is  a  closing  brace,  pop  the  last  opening  "brace"  off  the  stack    

o If  the  two  braces  are  not  a  matching  type  -­‐  error  • Repeat  until  the  is  stack  empty  or  reach  end  of  the  expression  

• If  you  run  out  of  symbols  before  stack  is  empty  -­‐  error  • If  the  stack  is  empty  before  the  end  of  the  expression  –  error  • Otherwise  the  expression  has  properly  nested  braces.    Below  the  stack  contents  is  shown  left-­‐to-­‐right  with  the  rightmost  symbol  on  the  top:    Eg,  {[()]()}  Stack  contents  each  round:  - empty  stack  { read  {    push  {[ read  [    push  {[( read  (    push  {[ read  )    pop  off  (  { read  ]    pop  off  [  {( read  (    push  { read  )    pop  off  (  - read  }    pop  off  {    Eg,    [{(})] Stack  contents  each  round:  - [ read  [    push  [{ read  {    push  [{( read  (    push  {[ read  }    pop  off  (    -­‐  error,  mismatch    Exercise:  Give  an  example  when  the  stack  is  empty  before  the  end  of  the  expression  and  another  when  the  stack  is  not  empty  but  all  of  the  expression  has  been  read.